knitting 0.1.62 → 0.1.70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +525 -335
- package/knitting.browser.js +1 -1
- package/map.md +0 -6
- package/package.json +3 -3
- package/prebuilds/darwin-arm64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-arm64-node-127/knitting_doorbell.node +0 -0
- package/prebuilds/darwin-arm64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-arm64-node-137/knitting_doorbell.node +0 -0
- package/prebuilds/darwin-x64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-x64-node-127/knitting_doorbell.node +0 -0
- package/prebuilds/darwin-x64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-x64-node-137/knitting_doorbell.node +0 -0
- package/prebuilds/linux-x64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/linux-x64-node-127/knitting_doorbell.node +0 -0
- package/prebuilds/linux-x64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/linux-x64-node-137/knitting_doorbell.node +0 -0
- package/prebuilds/win32-x64/knitting_windows_shared_memory.dll +0 -0
- package/prebuilds/win32-x64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/win32-x64-node-127/knitting_doorbell.node +0 -0
- package/prebuilds/win32-x64-node-127/knitting_shared_memory.node +0 -0
- package/prebuilds/win32-x64-node-127/knitting_shm.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_doorbell.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_shared_memory.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_shm.node +0 -0
- package/scripts/build-native-addons.ts +5 -0
- package/shared-memory.d.ts +3 -0
- package/shared-memory.js +3 -0
- package/src/api.js +109 -42
- package/src/common/with-resolvers.js +2 -5
- package/src/common/worker-runtime.d.ts +7 -0
- package/src/common/worker-runtime.js +7 -0
- package/src/connections/buffer-reference.d.ts +10 -36
- package/src/connections/buffer-reference.js +15 -170
- package/src/connections/node-addons.d.ts +1 -1
- package/src/connections/shared-array-buffer-payload.d.ts +7 -0
- package/src/connections/shared-array-buffer-payload.js +27 -11
- package/src/ipc/transport/shared-memory.d.ts +9 -1
- package/src/ipc/transport/shared-memory.js +13 -1
- package/src/knitting_buffer_pointer.cc +57 -2
- package/src/knitting_doorbell.cc +220 -0
- package/src/memory/knitting-body.d.ts +44 -0
- package/src/memory/knitting-body.js +51 -0
- package/src/memory/knitting-buffer-http.d.ts +116 -0
- package/src/memory/knitting-buffer-http.js +255 -0
- package/src/memory/knitting-buffer.d.ts +250 -0
- package/src/memory/knitting-buffer.js +695 -0
- package/src/memory/lazy-region-registry.d.ts +83 -0
- package/src/memory/lazy-region-registry.js +355 -0
- package/src/memory/lock.d.ts +38 -15
- package/src/memory/lock.js +227 -109
- package/src/memory/payloadCodec.d.ts +18 -2
- package/src/memory/payloadCodec.js +309 -65
- package/src/memory/regionRegistry.d.ts +6 -0
- package/src/memory/regionRegistry.js +125 -240
- package/src/memory/shared-buffer-io.d.ts +7 -0
- package/src/memory/shared-buffer-io.js +34 -8
- package/src/runtime/deno-doorbell.d.ts +26 -0
- package/src/runtime/deno-doorbell.js +117 -0
- package/src/runtime/dispatcher.d.ts +8 -6
- package/src/runtime/dispatcher.js +80 -58
- package/src/runtime/host-arg-arena.d.ts +3 -0
- package/src/runtime/host-arg-arena.js +16 -0
- package/src/runtime/node-doorbell.d.ts +14 -0
- package/src/runtime/node-doorbell.js +84 -0
- package/src/runtime/pool.d.ts +27 -15
- package/src/runtime/pool.js +138 -116
- package/src/runtime/process-worker.d.ts +9 -0
- package/src/runtime/process-worker.js +22 -2
- package/src/runtime/tx-queue.d.ts +2 -5
- package/src/runtime/tx-queue.js +52 -48
- package/src/runtime/worker-common.d.ts +2 -1
- package/src/runtime/worker-common.js +19 -5
- package/src/types.d.ts +36 -70
- package/src/worker/loop.js +95 -62
- package/src/worker/rx-queue.d.ts +2 -3
- package/src/worker/rx-queue.js +34 -40
- package/src/worker/shared-return.d.ts +9 -0
- package/src/worker/shared-return.js +22 -0
- package/src/worker/task-loader.js +1 -2
- package/src/worker/timers.d.ts +2 -6
- package/src/worker/timers.js +14 -19
- package/unsafe.d.ts +2 -1
- package/unsafe.js +2 -1
|
@@ -4,18 +4,9 @@ import { RUNTIME_IS_MAIN_THREAD } from "../common/worker-runtime.js";
|
|
|
4
4
|
import { loadNodeBufferPointerAddon } from "./node-buffer-pointer.js";
|
|
5
5
|
import { getBufferReferenceCapabilities, } from "./buffer-reference-native.js";
|
|
6
6
|
export const BUFFER_REFERENCE_KIND = "knitting.bufferReference";
|
|
7
|
-
/** Named values for the `unsafe.BufferReferenceReturn` pool option. */
|
|
8
|
-
export const BufferReferenceReturn = {
|
|
9
|
-
/** Safe default: Deno/Bun copy the returned bytes so they outlive the worker. */
|
|
10
|
-
Copy: "copy",
|
|
11
|
-
/** Zero-copy: borrow the worker's backing store until the reference is released. */
|
|
12
|
-
Borrow: "borrow",
|
|
13
|
-
};
|
|
14
7
|
export const BUFFER_REFERENCE_NUMERIC_TRANSFER = Symbol.for("knitting.bufferReference.numericTransfer");
|
|
15
|
-
export const BUFFER_REFERENCE_RETURN_RELEASE_TOKEN = Symbol.for("knitting.bufferReference.returnReleaseToken");
|
|
16
8
|
const EXTERNAL_PAYLOAD_BRAND = Symbol.for("knitting.payloadCodec");
|
|
17
9
|
export const BUFFER_REFERENCE_CODEC_ID = BUFFER_REFERENCE_KIND;
|
|
18
|
-
const BUFFER_REFERENCE_RETURN_RELEASE_MESSAGE_KEY = "__knittingBufferReferenceRelease";
|
|
19
10
|
const getRuntime = () => {
|
|
20
11
|
if (RUNTIME === "deno" || RUNTIME === "bun" || RUNTIME === "node") {
|
|
21
12
|
return RUNTIME;
|
|
@@ -37,7 +28,6 @@ const RUNTIME_NODE = 1;
|
|
|
37
28
|
const RUNTIME_DENO = 2;
|
|
38
29
|
const RUNTIME_BUN = 3;
|
|
39
30
|
const PAYLOAD_TRANSPORT_FINALIZER = Symbol.for("knitting.payloadCodec.transportFinalizer");
|
|
40
|
-
let currentReturnHooks;
|
|
41
31
|
const bufferReferenceInstances = new WeakSet();
|
|
42
32
|
const encodeRuntime = (runtime) => {
|
|
43
33
|
switch (runtime) {
|
|
@@ -96,36 +86,6 @@ const numericMetadataToMetadata = (words) => {
|
|
|
96
86
|
byteLength: words[5] ?? 0,
|
|
97
87
|
};
|
|
98
88
|
};
|
|
99
|
-
const toReturnHooks = (releaser) => typeof releaser === "function" ? { release: releaser } : releaser;
|
|
100
|
-
export const withBufferReferenceReturnReleaser = (releaser, run) => {
|
|
101
|
-
if (releaser === undefined)
|
|
102
|
-
return run();
|
|
103
|
-
const previous = currentReturnHooks;
|
|
104
|
-
currentReturnHooks = toReturnHooks(releaser);
|
|
105
|
-
try {
|
|
106
|
-
return run();
|
|
107
|
-
}
|
|
108
|
-
finally {
|
|
109
|
-
currentReturnHooks = previous;
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
export const createBufferReferenceReturnReleaseMessage = (token) => ({
|
|
113
|
-
[BUFFER_REFERENCE_RETURN_RELEASE_MESSAGE_KEY]: token.toString(),
|
|
114
|
-
});
|
|
115
|
-
export const readBufferReferenceReturnReleaseMessage = (value) => {
|
|
116
|
-
if (value === null || typeof value !== "object")
|
|
117
|
-
return undefined;
|
|
118
|
-
const raw = value[BUFFER_REFERENCE_RETURN_RELEASE_MESSAGE_KEY];
|
|
119
|
-
if (typeof raw !== "string")
|
|
120
|
-
return undefined;
|
|
121
|
-
try {
|
|
122
|
-
const token = BigInt(raw);
|
|
123
|
-
return token > 0n ? token : undefined;
|
|
124
|
-
}
|
|
125
|
-
catch {
|
|
126
|
-
return undefined;
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
89
|
const producerFinalizer = typeof FinalizationRegistry === "function"
|
|
130
90
|
? new FinalizationRegistry((backstop) => {
|
|
131
91
|
backstop.count -= 1;
|
|
@@ -139,16 +99,6 @@ const producerFinalizer = typeof FinalizationRegistry === "function"
|
|
|
139
99
|
}
|
|
140
100
|
})
|
|
141
101
|
: undefined;
|
|
142
|
-
const borrowedReturnFinalizer = typeof FinalizationRegistry === "function"
|
|
143
|
-
? new FinalizationRegistry(({ token, release }) => {
|
|
144
|
-
try {
|
|
145
|
-
release(token);
|
|
146
|
-
}
|
|
147
|
-
catch {
|
|
148
|
-
// best effort
|
|
149
|
-
}
|
|
150
|
-
})
|
|
151
|
-
: undefined;
|
|
152
102
|
const isSharedArrayBufferInstance = (value) => typeof SharedArrayBuffer === "function" && value instanceof SharedArrayBuffer;
|
|
153
103
|
const assertMovableSource = (source) => {
|
|
154
104
|
if (isSharedArrayBufferInstance(source)) {
|
|
@@ -166,6 +116,15 @@ const assertMovableSource = (source) => {
|
|
|
166
116
|
};
|
|
167
117
|
const isDetached = (buffer) => buffer.detached === true ||
|
|
168
118
|
buffer.byteLength === 0;
|
|
119
|
+
/**
|
|
120
|
+
* Whether a buffer has given up its bytes.
|
|
121
|
+
*
|
|
122
|
+
* Exported so a caller that *moved* a source can confirm the move actually
|
|
123
|
+
* happened. A runtime that cannot detach a buffer -- WASM memory, anything an
|
|
124
|
+
* external API has pinned -- otherwise leaves the producer still able to write
|
|
125
|
+
* memory the consumer has been told it owns.
|
|
126
|
+
*/
|
|
127
|
+
export const isArrayBufferDetached = (buffer) => isDetached(buffer);
|
|
169
128
|
/** Revocation primitive: detach `buffer` so every view over it is neutralized. */
|
|
170
129
|
export const detachArrayBufferBestEffort = (runtime, buffer) => {
|
|
171
130
|
if (isDetached(buffer))
|
|
@@ -241,9 +200,6 @@ export class BufferReference {
|
|
|
241
200
|
#producerBackstop;
|
|
242
201
|
#materializedRegions;
|
|
243
202
|
#owned;
|
|
244
|
-
#borrowedReturnReleaser;
|
|
245
|
-
#borrowedReturnTrack;
|
|
246
|
-
#borrowedReturnFinalizerToken;
|
|
247
203
|
#transportRefs = 0;
|
|
248
204
|
#released = false;
|
|
249
205
|
constructor(source, meta) {
|
|
@@ -341,9 +297,8 @@ export class BufferReference {
|
|
|
341
297
|
}
|
|
342
298
|
}
|
|
343
299
|
/** Prepare a returned reference before the worker-side producer hold drains. */
|
|
344
|
-
claimOwnership(
|
|
345
|
-
if (this.#owned !== undefined || this.#
|
|
346
|
-
this.#released)
|
|
300
|
+
claimOwnership() {
|
|
301
|
+
if (this.#owned !== undefined || this.#released)
|
|
347
302
|
return this;
|
|
348
303
|
this.#assertLocal();
|
|
349
304
|
const caps = getBufferReferenceCapabilities();
|
|
@@ -353,21 +308,6 @@ export class BufferReference {
|
|
|
353
308
|
byteOffset: this.byteOffset,
|
|
354
309
|
byteLength: this.byteLength,
|
|
355
310
|
};
|
|
356
|
-
if (releaser !== undefined) {
|
|
357
|
-
const hooks = toReturnHooks(releaser);
|
|
358
|
-
// Borrow: adopt lazily — the alias costs GC-visible external memory on
|
|
359
|
-
// FFI runtimes, so it is only created when the bytes are actually read.
|
|
360
|
-
// Until then no view can exist, so the GC backstop safely watches the
|
|
361
|
-
// reference itself; first materialize migrates it to the alias buffer
|
|
362
|
-
// (views strongly reference their buffer, so the worker pin can only
|
|
363
|
-
// drop once no view can reach the borrowed bytes).
|
|
364
|
-
this.#borrowedReturnReleaser = hooks.release;
|
|
365
|
-
this.#borrowedReturnTrack = hooks.track;
|
|
366
|
-
this.#borrowedReturnFinalizerToken = {};
|
|
367
|
-
borrowedReturnFinalizer?.register(this, { token: this.#token, release: hooks.release }, this.#borrowedReturnFinalizerToken);
|
|
368
|
-
hooks.track?.(this, this.#token);
|
|
369
|
-
return this;
|
|
370
|
-
}
|
|
371
311
|
this.#owned = caps.adopt(input, caps.supportsOwningAdopt ? undefined : { copy: true });
|
|
372
312
|
return this;
|
|
373
313
|
}
|
|
@@ -382,18 +322,6 @@ export class BufferReference {
|
|
|
382
322
|
byteOffset: this.byteOffset,
|
|
383
323
|
byteLength: this.byteLength,
|
|
384
324
|
});
|
|
385
|
-
if (this.#borrowedReturnReleaser !== undefined) {
|
|
386
|
-
// First borrowed read: views over this alias exist from here on, so
|
|
387
|
-
// move the GC backstop from the reference onto the alias buffer.
|
|
388
|
-
this.#owned = region;
|
|
389
|
-
if (this.#borrowedReturnFinalizerToken !== undefined &&
|
|
390
|
-
borrowedReturnFinalizer !== undefined) {
|
|
391
|
-
borrowedReturnFinalizer.unregister(this.#borrowedReturnFinalizerToken);
|
|
392
|
-
borrowedReturnFinalizer.register(region.buffer, { token: this.#token, release: this.#borrowedReturnReleaser }, this.#borrowedReturnFinalizerToken);
|
|
393
|
-
}
|
|
394
|
-
this.#borrowedReturnTrack?.(this, this.#token, region.buffer);
|
|
395
|
-
return region;
|
|
396
|
-
}
|
|
397
325
|
(this.#materializedRegions ??= []).push(region);
|
|
398
326
|
if (this.#producerBackstop !== undefined && producerFinalizer !== undefined) {
|
|
399
327
|
this.#producerBackstop.count += 1;
|
|
@@ -420,6 +348,10 @@ export class BufferReference {
|
|
|
420
348
|
if (this.#released)
|
|
421
349
|
return;
|
|
422
350
|
this.#released = true;
|
|
351
|
+
// A claimed Node return is an owning ArrayBuffer, not a borrow. Drop this
|
|
352
|
+
// wrapper's ownership immediately; any ArrayBuffer or typed-array the
|
|
353
|
+
// caller kept still co-owns the backing store and stays valid.
|
|
354
|
+
this.#owned = undefined;
|
|
423
355
|
// Revoke every alias this reference handed out before any pin can drop.
|
|
424
356
|
// Leak-not-UAF: if a detach fails, the pin must stay held — the GC
|
|
425
357
|
// backstops remain registered and retry once the buffers are unreachable.
|
|
@@ -433,31 +365,6 @@ export class BufferReference {
|
|
|
433
365
|
allDetached;
|
|
434
366
|
}
|
|
435
367
|
}
|
|
436
|
-
const borrowedReturnReleaser = this.#borrowedReturnReleaser;
|
|
437
|
-
this.#borrowedReturnReleaser = undefined;
|
|
438
|
-
this.#borrowedReturnTrack = undefined;
|
|
439
|
-
if (borrowedReturnReleaser !== undefined) {
|
|
440
|
-
const owned = this.#owned;
|
|
441
|
-
this.#owned = undefined;
|
|
442
|
-
if (owned !== undefined) {
|
|
443
|
-
allDetached = detachArrayBufferBestEffort(this.runtime, owned.buffer) &&
|
|
444
|
-
allDetached;
|
|
445
|
-
}
|
|
446
|
-
if (allDetached) {
|
|
447
|
-
if (this.#borrowedReturnFinalizerToken !== undefined) {
|
|
448
|
-
borrowedReturnFinalizer?.unregister(this.#borrowedReturnFinalizerToken);
|
|
449
|
-
this.#borrowedReturnFinalizerToken = undefined;
|
|
450
|
-
}
|
|
451
|
-
try {
|
|
452
|
-
borrowedReturnReleaser(this.#token);
|
|
453
|
-
}
|
|
454
|
-
catch {
|
|
455
|
-
// best effort
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
// else: the finalizer stays registered on the alias buffer and sends
|
|
459
|
-
// the release once every escaped view is gone.
|
|
460
|
-
}
|
|
461
368
|
// Only the producer owns the registry hold; consumers must not drop it.
|
|
462
369
|
if (this.#isProducer && allDetached) {
|
|
463
370
|
if (this.#finalizerToken !== undefined) {
|
|
@@ -475,65 +382,6 @@ export class BufferReference {
|
|
|
475
382
|
// else on failed detach: the countdown backstop drops the pin when the
|
|
476
383
|
// reference and every materialized alias are unreachable.
|
|
477
384
|
}
|
|
478
|
-
/**
|
|
479
|
-
* Revoke a borrowed return before its producing worker goes away: detach
|
|
480
|
-
* the borrowed alias so no view can dangle. With `copyBytes` the reference
|
|
481
|
-
* itself survives on an owned copy; without it (worker bytes already gone)
|
|
482
|
-
* later reads fail loud instead of aliasing freed memory.
|
|
483
|
-
*/
|
|
484
|
-
revokeBorrow(copyBytes) {
|
|
485
|
-
const releaser = this.#borrowedReturnReleaser;
|
|
486
|
-
if (releaser === undefined || this.#released)
|
|
487
|
-
return;
|
|
488
|
-
const owned = this.#owned;
|
|
489
|
-
let nextOwned;
|
|
490
|
-
let detached = true;
|
|
491
|
-
if (owned !== undefined) {
|
|
492
|
-
if (copyBytes) {
|
|
493
|
-
nextOwned = {
|
|
494
|
-
buffer: owned.buffer.slice(owned.byteOffset, owned.byteOffset + owned.byteLength),
|
|
495
|
-
byteOffset: 0,
|
|
496
|
-
byteLength: owned.byteLength,
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
// Without a copy, keep the (now detached) region so reads throw
|
|
500
|
-
// instead of re-materializing an alias over freed worker memory.
|
|
501
|
-
detached = detachArrayBufferBestEffort(this.runtime, owned.buffer);
|
|
502
|
-
}
|
|
503
|
-
else if (copyBytes) {
|
|
504
|
-
// Never materialized: take the bytes now, while the worker still pins
|
|
505
|
-
// them, so the reference stays readable after its worker dies.
|
|
506
|
-
const caps = getBufferReferenceCapabilities();
|
|
507
|
-
nextOwned = caps.adopt({
|
|
508
|
-
token: this.#token,
|
|
509
|
-
pointer: this.pointer,
|
|
510
|
-
byteOffset: this.byteOffset,
|
|
511
|
-
byteLength: this.byteLength,
|
|
512
|
-
}, { copy: !caps.supportsOwningAdopt });
|
|
513
|
-
}
|
|
514
|
-
else {
|
|
515
|
-
// Never materialized and the worker bytes are already gone: no view
|
|
516
|
-
// exists, so poison future reads instead of aliasing freed memory.
|
|
517
|
-
this.#released = true;
|
|
518
|
-
}
|
|
519
|
-
if (!detached)
|
|
520
|
-
return;
|
|
521
|
-
if (nextOwned !== undefined)
|
|
522
|
-
this.#owned = nextOwned;
|
|
523
|
-
this.#borrowedReturnReleaser = undefined;
|
|
524
|
-
this.#borrowedReturnTrack = undefined;
|
|
525
|
-
if (this.#borrowedReturnFinalizerToken !== undefined) {
|
|
526
|
-
borrowedReturnFinalizer?.unregister(this.#borrowedReturnFinalizerToken);
|
|
527
|
-
this.#borrowedReturnFinalizerToken = undefined;
|
|
528
|
-
}
|
|
529
|
-
// Best effort: if the worker is still alive it can drop its pin early.
|
|
530
|
-
try {
|
|
531
|
-
releaser(this.#token);
|
|
532
|
-
}
|
|
533
|
-
catch {
|
|
534
|
-
// best effort
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
385
|
[Symbol.dispose]() {
|
|
538
386
|
this.release();
|
|
539
387
|
}
|
|
@@ -550,9 +398,6 @@ export class BufferReference {
|
|
|
550
398
|
if (this.#transportRefs <= 0)
|
|
551
399
|
this.release();
|
|
552
400
|
});
|
|
553
|
-
if (this.#isProducer) {
|
|
554
|
-
finalizer[BUFFER_REFERENCE_RETURN_RELEASE_TOKEN] = this.#token;
|
|
555
|
-
}
|
|
556
401
|
return finalizer;
|
|
557
402
|
}
|
|
558
403
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
type NodeRequire = (specifier: string) => unknown;
|
|
2
|
-
export type NodeNativeAddonName = "knitting_shared_memory" | "knitting_shm" | "knitting_buffer_pointer";
|
|
2
|
+
export type NodeNativeAddonName = "knitting_shared_memory" | "knitting_shm" | "knitting_buffer_pointer" | "knitting_doorbell";
|
|
3
3
|
export type NodePlatformInfo = {
|
|
4
4
|
arch: string;
|
|
5
5
|
modules?: string;
|
|
@@ -30,6 +30,13 @@ type SharedArrayBufferPayload = {
|
|
|
30
30
|
readonly [SHARED_ARRAY_BUFFER_NUMERIC_TRANSFER]: (transportKey?: object) => SharedArrayBufferNumericMetadata | SharedArrayBufferTokenNumericMetadata | undefined;
|
|
31
31
|
};
|
|
32
32
|
export declare const isSharedArrayBufferValue: (value: unknown) => value is SharedArrayBuffer;
|
|
33
|
+
type SharedPin = {
|
|
34
|
+
token: bigint;
|
|
35
|
+
pointer: bigint;
|
|
36
|
+
byteLength: number;
|
|
37
|
+
};
|
|
38
|
+
/** The pin backing `buffer`, when it has been shared at least once. */
|
|
39
|
+
export declare const getSharedPin: (buffer: object) => SharedPin | undefined;
|
|
33
40
|
/** Wrap a SAB as external payload; GC-managed pins mean no settle finalizer. */
|
|
34
41
|
export declare const wrapSharedArrayBufferPayload: (sab: SharedArrayBuffer) => SharedArrayBufferPayload;
|
|
35
42
|
export declare const getSharedArrayBufferPayload: (value: object) => SharedArrayBufferPayload | undefined;
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { RUNTIME } from "../common/runtime.js";
|
|
2
2
|
import { getNodeProcess } from "../common/node-compat.js";
|
|
3
3
|
import { getBufferReferenceCapabilities } from "./buffer-reference-native.js";
|
|
4
|
-
//
|
|
5
|
-
// SABs are
|
|
4
|
+
// SharedArrayBuffer transport uses a process-local pointer and is limited to
|
|
5
|
+
// thread workers; SABs are not moved or detached.
|
|
6
6
|
export const SHARED_ARRAY_BUFFER_CODEC_ID = "knitting.sharedArrayBuffer";
|
|
7
7
|
export const SHARED_ARRAY_BUFFER_NUMERIC_TRANSFER = Symbol.for("knitting.sharedArrayBuffer.numericTransfer");
|
|
8
8
|
export const SHARED_ARRAY_BUFFER_NUMERIC_WORDS = 8;
|
|
9
9
|
const SHARED_ARRAY_BUFFER_TOKEN_NUMERIC_WORDS = 2;
|
|
10
|
+
// Slice frames carry an explicit length; warm frames carry only the token.
|
|
10
11
|
const EXTERNAL_PAYLOAD_BRAND = Symbol.for("knitting.payloadCodec");
|
|
11
12
|
const getProcessId = () => {
|
|
12
13
|
const proc = getNodeProcess();
|
|
@@ -23,7 +24,19 @@ export const isSharedArrayBufferValue = (value) => hasSharedArrayBuffer && value
|
|
|
23
24
|
const pinnedBySab = new WeakMap();
|
|
24
25
|
const payloadBySharedBuffer = new WeakMap();
|
|
25
26
|
const warmedTokensByTransport = new WeakMap();
|
|
26
|
-
|
|
27
|
+
// Tokens are isolate-local, so adopted aliases are cached per transport lane.
|
|
28
|
+
const adoptedByTransport = new WeakMap();
|
|
29
|
+
const adoptedWithoutTransport = new Map();
|
|
30
|
+
const adoptedCacheFor = (transportKey) => {
|
|
31
|
+
if (transportKey === undefined)
|
|
32
|
+
return adoptedWithoutTransport;
|
|
33
|
+
let cache = adoptedByTransport.get(transportKey);
|
|
34
|
+
if (cache === undefined) {
|
|
35
|
+
cache = new Map();
|
|
36
|
+
adoptedByTransport.set(transportKey, cache);
|
|
37
|
+
}
|
|
38
|
+
return cache;
|
|
39
|
+
};
|
|
27
40
|
const pinFinalizer = typeof FinalizationRegistry === "function"
|
|
28
41
|
? new FinalizationRegistry((token) => {
|
|
29
42
|
try {
|
|
@@ -87,6 +100,8 @@ const pinSab = (sab) => {
|
|
|
87
100
|
}
|
|
88
101
|
return pin;
|
|
89
102
|
};
|
|
103
|
+
/** The pin backing `buffer`, when it has been shared at least once. */
|
|
104
|
+
export const getSharedPin = (buffer) => pinnedBySab.get(buffer);
|
|
90
105
|
const makeMetadata = (pin) => ({
|
|
91
106
|
kind: SHARED_ARRAY_BUFFER_CODEC_ID,
|
|
92
107
|
origin: PROCESS_ORIGIN,
|
|
@@ -167,13 +182,14 @@ const isSharedArrayBufferMetadata = (value) => {
|
|
|
167
182
|
Number.isInteger(meta.byteLength) &&
|
|
168
183
|
meta.byteLength >= 0);
|
|
169
184
|
};
|
|
170
|
-
const materializeSharedBuffer = (metadata, warmOnly) => {
|
|
185
|
+
const materializeSharedBuffer = (metadata, warmOnly, transportKey) => {
|
|
171
186
|
if (metadata.origin !== PROCESS_ORIGIN) {
|
|
172
187
|
throw new Error(`SharedArrayBuffer cannot cross a process boundary (origin ${metadata.origin} ` +
|
|
173
188
|
`!= ${PROCESS_ORIGIN}); it is shared by reference to thread workers only.`);
|
|
174
189
|
}
|
|
175
190
|
const token = BigInt(metadata.token);
|
|
176
|
-
const
|
|
191
|
+
const cache = adoptedCacheFor(transportKey);
|
|
192
|
+
const cached = cache.get(token);
|
|
177
193
|
if (cached !== undefined)
|
|
178
194
|
return cached;
|
|
179
195
|
if (warmOnly) {
|
|
@@ -185,7 +201,7 @@ const materializeSharedBuffer = (metadata, warmOnly) => {
|
|
|
185
201
|
byteOffset: 0,
|
|
186
202
|
byteLength: metadata.byteLength,
|
|
187
203
|
});
|
|
188
|
-
|
|
204
|
+
cache.set(token, region.buffer);
|
|
189
205
|
createSharedArrayBufferPayload(region.buffer, {
|
|
190
206
|
token,
|
|
191
207
|
pointer: BigInt(metadata.pointer),
|
|
@@ -193,16 +209,16 @@ const materializeSharedBuffer = (metadata, warmOnly) => {
|
|
|
193
209
|
}, metadata);
|
|
194
210
|
return region.buffer;
|
|
195
211
|
};
|
|
196
|
-
const decode = (metadata) => {
|
|
212
|
+
const decode = (metadata, transportKey) => {
|
|
197
213
|
if (!isSharedArrayBufferMetadata(metadata)) {
|
|
198
214
|
throw new TypeError("Invalid SharedArrayBuffer payload metadata");
|
|
199
215
|
}
|
|
200
|
-
return materializeSharedBuffer(metadata, false);
|
|
216
|
+
return materializeSharedBuffer(metadata, false, transportKey);
|
|
201
217
|
};
|
|
202
|
-
const decodeNumeric = (words) => {
|
|
218
|
+
const decodeNumeric = (words, transportKey) => {
|
|
203
219
|
if (words.length === SHARED_ARRAY_BUFFER_TOKEN_NUMERIC_WORDS) {
|
|
204
220
|
const token = joinU64(words[0] ?? 0, words[1] ?? 0);
|
|
205
|
-
const cached =
|
|
221
|
+
const cached = adoptedCacheFor(transportKey).get(token);
|
|
206
222
|
if (cached !== undefined)
|
|
207
223
|
return cached;
|
|
208
224
|
throw new TypeError("SharedArrayBuffer cache miss for warm token payload");
|
|
@@ -228,7 +244,7 @@ const decodeNumeric = (words) => {
|
|
|
228
244
|
token: joinU64(words[0] ?? 0, words[1] ?? 0).toString(),
|
|
229
245
|
byteLength: words[4] ?? 0,
|
|
230
246
|
};
|
|
231
|
-
return materializeSharedBuffer(metadata, false);
|
|
247
|
+
return materializeSharedBuffer(metadata, false, transportKey);
|
|
232
248
|
};
|
|
233
249
|
const codecGlobal = globalThis;
|
|
234
250
|
const codecs = codecGlobal.__KNITTING_PAYLOAD_CODECS__ ??= Object.create(null);
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
export type SignalArguments = ReturnType<typeof createSharedMemoryTransport>;
|
|
2
2
|
import { type SharedBufferSource } from "../../common/shared-buffer-region.js";
|
|
3
3
|
export declare const TRANSPORT_SIGNAL_BYTES: number;
|
|
4
|
+
/** States of the worker-stop word. */
|
|
5
|
+
export declare const WORKER_STOP: {
|
|
6
|
+
readonly running: 0;
|
|
7
|
+
readonly requested: 1;
|
|
8
|
+
/** The worker has left its dispatch loop. */
|
|
9
|
+
readonly acknowledged: 2;
|
|
10
|
+
};
|
|
4
11
|
export type Sab = {
|
|
5
12
|
size?: number;
|
|
6
13
|
sharedSab?: SharedBufferSource;
|
|
@@ -18,6 +25,7 @@ export declare const createSharedMemoryTransport: ({ sabObject, isMain, startTim
|
|
|
18
25
|
opView: Int32Array<import("../../common/shared-buffer-region.js").SharedBuffer>;
|
|
19
26
|
rxStatus: Int32Array<import("../../common/shared-buffer-region.js").SharedBuffer>;
|
|
20
27
|
txStatus: Int32Array<import("../../common/shared-buffer-region.js").SharedBuffer>;
|
|
28
|
+
stopView: Int32Array<import("../../common/shared-buffer-region.js").SharedBuffer>;
|
|
21
29
|
};
|
|
22
|
-
export type MainSignal = Pick<SignalArguments, "opView" | "startAt" | "rxStatus" | "txStatus">;
|
|
30
|
+
export type MainSignal = Pick<SignalArguments, "opView" | "startAt" | "rxStatus" | "txStatus" | "stopView">;
|
|
23
31
|
export {};
|
|
@@ -7,8 +7,16 @@ const SIGNAL_OFFSETS = {
|
|
|
7
7
|
op: 0,
|
|
8
8
|
rxStatus: CACHE_LINE_BYTES,
|
|
9
9
|
txStatus: CACHE_LINE_BYTES * 2,
|
|
10
|
+
stop: CACHE_LINE_BYTES * 3,
|
|
11
|
+
};
|
|
12
|
+
export const TRANSPORT_SIGNAL_BYTES = CACHE_LINE_BYTES * 4;
|
|
13
|
+
/** States of the worker-stop word. */
|
|
14
|
+
export const WORKER_STOP = {
|
|
15
|
+
running: 0,
|
|
16
|
+
requested: 1,
|
|
17
|
+
/** The worker has left its dispatch loop. */
|
|
18
|
+
acknowledged: 2,
|
|
10
19
|
};
|
|
11
|
-
export const TRANSPORT_SIGNAL_BYTES = CACHE_LINE_BYTES * 3;
|
|
12
20
|
const a_store = Atomics.store;
|
|
13
21
|
export const createSharedMemoryTransport = ({ sabObject, isMain, startTime }) => {
|
|
14
22
|
const toGrow = sabObject?.size ?? page;
|
|
@@ -24,6 +32,9 @@ export const createSharedMemoryTransport = ({ sabObject, isMain, startTime }) =>
|
|
|
24
32
|
a_store(opView, 0, 0);
|
|
25
33
|
const rxStatus = new Int32Array(sab, baseByteOffset + SIGNAL_OFFSETS.rxStatus, 1);
|
|
26
34
|
a_store(rxStatus, 0, 1);
|
|
35
|
+
const stopView = new Int32Array(sab, baseByteOffset + SIGNAL_OFFSETS.stop, 1);
|
|
36
|
+
if (isMain)
|
|
37
|
+
a_store(stopView, 0, WORKER_STOP.running);
|
|
27
38
|
return {
|
|
28
39
|
sab: signalRegion,
|
|
29
40
|
op: opView,
|
|
@@ -31,5 +42,6 @@ export const createSharedMemoryTransport = ({ sabObject, isMain, startTime }) =>
|
|
|
31
42
|
opView,
|
|
32
43
|
rxStatus,
|
|
33
44
|
txStatus: new Int32Array(sab, baseByteOffset + SIGNAL_OFFSETS.txStatus, 1),
|
|
45
|
+
stopView,
|
|
34
46
|
};
|
|
35
47
|
};
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
#include <memory>
|
|
10
10
|
#include <mutex>
|
|
11
11
|
#include <unordered_map>
|
|
12
|
+
#include <vector>
|
|
12
13
|
|
|
13
14
|
namespace knitting_buffer_pointer {
|
|
14
15
|
|
|
@@ -27,12 +28,51 @@ struct RetainedBackingStore {
|
|
|
27
28
|
std::shared_ptr<v8::BackingStore> store;
|
|
28
29
|
size_t byte_offset;
|
|
29
30
|
size_t byte_length;
|
|
31
|
+
// The isolate that produced the move. This is only a registry hold: a
|
|
32
|
+
// consumer's adopted ArrayBuffer owns its own shared_ptr.
|
|
33
|
+
v8::Isolate* producer_isolate;
|
|
30
34
|
};
|
|
31
35
|
|
|
32
36
|
std::mutex backing_mutex;
|
|
33
37
|
uint64_t next_backing_id = 1;
|
|
34
38
|
std::unordered_map<uint64_t, RetainedBackingStore> retained_backings;
|
|
35
39
|
|
|
40
|
+
// Node tears a worker down without running its JavaScript finalizers. Both
|
|
41
|
+
// registries are process-global, so discard holds produced by that environment
|
|
42
|
+
// during teardown. Any consumer that adopted a backing store already has an
|
|
43
|
+
// independent shared_ptr and is unaffected.
|
|
44
|
+
void CleanupEnvironment(void* data) {
|
|
45
|
+
v8::Isolate* isolate = static_cast<v8::Isolate*>(data);
|
|
46
|
+
// Collect first, destroy on scope exit: releasing a hold must not run under
|
|
47
|
+
// the registry lock that found it.
|
|
48
|
+
std::vector<std::unique_ptr<RetainedReference>> released_refs;
|
|
49
|
+
std::vector<RetainedBackingStore> released_backings;
|
|
50
|
+
|
|
51
|
+
{
|
|
52
|
+
std::lock_guard<std::mutex> lock(retained_mutex);
|
|
53
|
+
for (auto it = retained_refs.begin(); it != retained_refs.end();) {
|
|
54
|
+
if (it->second->isolate != isolate) {
|
|
55
|
+
++it;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
released_refs.push_back(std::move(it->second));
|
|
59
|
+
it = retained_refs.erase(it);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
{
|
|
64
|
+
std::lock_guard<std::mutex> lock(backing_mutex);
|
|
65
|
+
for (auto it = retained_backings.begin(); it != retained_backings.end();) {
|
|
66
|
+
if (it->second.producer_isolate != isolate) {
|
|
67
|
+
++it;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
released_backings.push_back(std::move(it->second));
|
|
71
|
+
it = retained_backings.erase(it);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
36
76
|
void ThrowType(v8::Isolate* isolate, const char* message) {
|
|
37
77
|
isolate->ThrowException(v8::Exception::TypeError(
|
|
38
78
|
v8::String::NewFromUtf8(isolate, message).ToLocalChecked()
|
|
@@ -318,7 +358,18 @@ void RetainBackingStore(const v8::FunctionCallbackInfo<v8::Value>& args) {
|
|
|
318
358
|
uintptr_t address = reinterpret_cast<uintptr_t>(base + byte_offset);
|
|
319
359
|
|
|
320
360
|
// Detach only after taking the shared_ptr; ordinary JS buffers use no key.
|
|
321
|
-
|
|
361
|
+
// A failed detach must not be ignored: retaining the store while the source
|
|
362
|
+
// stays live hands the consumer an "owned" buffer that still aliases memory
|
|
363
|
+
// the producer can write. WASM memory and externally pinned buffers are the
|
|
364
|
+
// ones that land here, and the caller falls back to copying.
|
|
365
|
+
if (!DetachDefaultArrayBuffer(buffer)) {
|
|
366
|
+
ThrowType(
|
|
367
|
+
isolate,
|
|
368
|
+
"retainBackingStore requires a detachable ArrayBuffer; this buffer "
|
|
369
|
+
"cannot be moved without leaving the source aliasing it"
|
|
370
|
+
);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
322
373
|
|
|
323
374
|
uint64_t token = 0;
|
|
324
375
|
{
|
|
@@ -327,7 +378,9 @@ void RetainBackingStore(const v8::FunctionCallbackInfo<v8::Value>& args) {
|
|
|
327
378
|
if (token == 0) token = next_backing_id++;
|
|
328
379
|
retained_backings.emplace(
|
|
329
380
|
token,
|
|
330
|
-
RetainedBackingStore{
|
|
381
|
+
RetainedBackingStore{
|
|
382
|
+
std::move(store), byte_offset, byte_length, isolate
|
|
383
|
+
}
|
|
331
384
|
);
|
|
332
385
|
}
|
|
333
386
|
|
|
@@ -410,6 +463,8 @@ void Initialize(
|
|
|
410
463
|
v8::Local<v8::Value>,
|
|
411
464
|
v8::Local<v8::Context>
|
|
412
465
|
) {
|
|
466
|
+
v8::Isolate* isolate = exports->GetIsolate();
|
|
467
|
+
node::AddEnvironmentCleanupHook(isolate, CleanupEnvironment, isolate);
|
|
413
468
|
NODE_SET_METHOD(exports, "getPointer", GetPointer);
|
|
414
469
|
NODE_SET_METHOD(exports, "retainPointer", RetainPointer);
|
|
415
470
|
NODE_SET_METHOD(exports, "releasePointer", ReleasePointer);
|