knitting 0.1.56 → 0.1.61
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/BROWSER.md +134 -0
- package/README.md +347 -46
- package/knitting.browser.d.ts +6 -0
- package/knitting.browser.js +1 -0
- package/knitting.d.ts +3 -2
- package/knitting.js +3 -3
- package/map.md +21 -9
- package/package.json +17 -2
- package/prebuilds/darwin-arm64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-arm64-node-127/knitting_shared_memory.node +0 -0
- package/prebuilds/darwin-arm64-node-127/knitting_shm.node +0 -0
- package/prebuilds/darwin-arm64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-arm64-node-137/knitting_shared_memory.node +0 -0
- package/prebuilds/darwin-arm64-node-137/knitting_shm.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_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_shared_memory.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_shm.node +0 -0
- package/scripts/build-compiled-worker.ts +276 -0
- package/scripts/compiled-worker/porffor.d.ts +5 -0
- package/scripts/compiled-worker/runtime.ts +529 -0
- package/scripts/compiled-worker/task-shim.ts +32 -0
- package/src/api.d.ts +24 -1
- package/src/api.js +311 -27
- package/src/common/module-url.d.ts +1 -0
- package/src/common/module-url.js +30 -2
- package/src/common/node-compat.d.ts +2 -0
- package/src/common/path-canonical.js +20 -6
- package/src/common/runtime.d.ts +3 -1
- package/src/common/runtime.js +25 -4
- package/src/common/task-source.d.ts +2 -0
- package/src/common/task-source.js +17 -1
- package/src/common/worker-runtime.js +16 -7
- package/src/connections/buffer-reference-native.js +25 -1
- package/src/connections/buffer-reference.d.ts +23 -2
- package/src/connections/buffer-reference.js +143 -29
- package/src/connections/bun.d.ts +1 -1
- package/src/connections/bun.js +25 -14
- package/src/connections/deno.d.ts +3 -1
- package/src/connections/deno.js +35 -14
- package/src/connections/external-array-buffer.d.ts +2 -0
- package/src/connections/external-array-buffer.js +33 -0
- package/src/connections/node-addons.d.ts +11 -0
- package/src/connections/node-addons.js +49 -1
- package/src/connections/node-ffi-api.d.ts +19 -0
- package/src/connections/node-ffi-api.js +28 -0
- package/src/connections/node-ffi.d.ts +27 -0
- package/src/connections/node-ffi.js +262 -0
- package/src/connections/node.d.ts +1 -1
- package/src/connections/node.js +20 -20
- package/src/connections/package-assets.js +25 -15
- package/src/connections/posix.d.ts +42 -0
- package/src/connections/posix.js +101 -0
- package/src/connections/process-shared-buffer.d.ts +1 -0
- package/src/connections/process-shared-buffer.js +11 -45
- package/src/connections/types.d.ts +4 -0
- package/src/connections/windows.d.ts +19 -10
- package/src/connections/windows.js +87 -66
- package/src/error.d.ts +7 -6
- package/src/error.js +8 -7
- package/src/memory/lock.d.ts +112 -77
- package/src/memory/lock.js +314 -87
- package/src/memory/payloadCodec.d.ts +11 -2
- package/src/memory/payloadCodec.js +129 -37
- package/src/memory/regionRegistry.d.ts +1 -3
- package/src/memory/regionRegistry.js +16 -14
- package/src/permission/compatibility.d.ts +28 -0
- package/src/permission/compatibility.js +239 -0
- package/src/permission/index.d.ts +2 -0
- package/src/permission/index.js +1 -0
- package/src/permission/protocol.d.ts +1 -0
- package/src/permission/protocol.js +59 -15
- package/src/runtime/compiled-artifact.d.ts +21 -0
- package/src/runtime/compiled-artifact.js +260 -0
- package/src/runtime/compiled-builder.d.ts +8 -0
- package/src/runtime/compiled-builder.js +42 -0
- package/src/runtime/compiled-worker.d.ts +27 -0
- package/src/runtime/compiled-worker.js +562 -0
- package/src/runtime/dispatcher.js +9 -0
- package/src/runtime/inline-executor.js +27 -15
- package/src/runtime/pool.d.ts +121 -5
- package/src/runtime/pool.js +263 -49
- package/src/runtime/process-worker.d.ts +3 -13
- package/src/runtime/process-worker.js +97 -101
- package/src/runtime/tx-queue.d.ts +18 -2
- package/src/runtime/tx-queue.js +56 -16
- package/src/runtime/worker-common.d.ts +13 -0
- package/src/runtime/worker-common.js +102 -0
- package/src/types.d.ts +89 -9
- package/src/worker/composable-runners.js +3 -2
- package/src/worker/loop.d.ts +1 -0
- package/src/worker/loop.js +52 -17
- package/src/worker/rx-queue.d.ts +11 -1
- package/src/worker/rx-queue.js +4 -1
- package/src/worker/task-loader.d.ts +5 -4
- package/src/worker/task-loader.js +13 -7
- package/src/worker/timers.d.ts +8 -1
- package/src/worker/timers.js +28 -23
package/src/memory/lock.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import RingQueue from "../ipc/tools/ring-queue.js";
|
|
2
|
-
|
|
2
|
+
let registeredEncodePayload;
|
|
3
|
+
let registeredDecodePayload;
|
|
4
|
+
export const registerLockPayloadCodec = (encode, decode) => {
|
|
5
|
+
registeredEncodePayload = encode;
|
|
6
|
+
registeredDecodePayload = decode;
|
|
7
|
+
};
|
|
3
8
|
import { createSharedArrayBuffer, createWasmSharedArrayBuffer, } from "../common/runtime.js";
|
|
4
9
|
import { toSharedBufferRegion, } from "../common/shared-buffer-region.js";
|
|
5
10
|
import { probeLockBufferTextCompat, } from "../common/shared-buffer-text.js";
|
|
@@ -7,69 +12,76 @@ import { resolvePayloadBufferOptions, } from "./payload-config.js";
|
|
|
7
12
|
/**
|
|
8
13
|
* TODO: Compose all the instance where the array is passed as argument
|
|
9
14
|
*/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
15
|
+
// const objects replace `enum`s throughout this module: Andromeda's Nova engine
|
|
16
|
+
// panics on `enum`. Value access, duplicate-value members, and type preserved;
|
|
17
|
+
// identical emit on Node/Deno/Bun.
|
|
18
|
+
export const PayloadSignal = {
|
|
19
|
+
UNREACHABLE: 0,
|
|
20
|
+
BigInt: 2,
|
|
21
|
+
True: 3,
|
|
22
|
+
False: 4,
|
|
23
|
+
Undefined: 5,
|
|
24
|
+
NaN: 6,
|
|
25
|
+
Float64: 9,
|
|
26
|
+
Null: 10,
|
|
27
|
+
};
|
|
28
|
+
export const PayloadBuffer = {
|
|
29
|
+
BORDER_SIGNAL_BUFFER: 11,
|
|
30
|
+
String: 11,
|
|
31
|
+
Json: 12,
|
|
32
|
+
StaticString: 15,
|
|
33
|
+
StaticJson: 16,
|
|
34
|
+
Binary: 17,
|
|
35
|
+
StaticBinary: 18,
|
|
36
|
+
Int32Array: 19,
|
|
37
|
+
Float64Array: 20,
|
|
38
|
+
BigInt64Array: 21,
|
|
39
|
+
BigUint64Array: 22,
|
|
40
|
+
DataView: 23,
|
|
41
|
+
Error: 24,
|
|
42
|
+
Date: 25,
|
|
43
|
+
Symbol: 26,
|
|
44
|
+
StaticSymbol: 27,
|
|
45
|
+
BigInt: 28,
|
|
46
|
+
StaticBigInt: 29,
|
|
47
|
+
StaticInt32Array: 31,
|
|
48
|
+
StaticFloat64Array: 32,
|
|
49
|
+
StaticBigInt64Array: 33,
|
|
50
|
+
StaticBigUint64Array: 34,
|
|
51
|
+
StaticDataView: 35,
|
|
52
|
+
ArrayBuffer: 36,
|
|
53
|
+
StaticArrayBuffer: 37,
|
|
54
|
+
Buffer: 38,
|
|
55
|
+
StaticBuffer: 39,
|
|
56
|
+
EnvelopeStaticHeader: 40,
|
|
57
|
+
EnvelopeDynamicHeader: 41,
|
|
58
|
+
EnvelopeStaticHeaderString: 42,
|
|
59
|
+
EnvelopeDynamicHeaderString: 43,
|
|
60
|
+
ExternalPayload: 44,
|
|
61
|
+
StaticExternalPayload: 45,
|
|
62
|
+
ProcessSharedBuffer: 46,
|
|
63
|
+
BufferReference: 47,
|
|
64
|
+
SharedArrayBuffer: 48,
|
|
65
|
+
EnvelopeStaticHeaderExternal: 49,
|
|
66
|
+
EnvelopeDynamicHeaderExternal: 50,
|
|
67
|
+
EnvelopeStaticHeaderStringExternal: 51,
|
|
68
|
+
EnvelopeDynamicHeaderStringExternal: 52,
|
|
69
|
+
NumericArray: 53,
|
|
70
|
+
StaticNumericArray: 54,
|
|
71
|
+
};
|
|
72
|
+
// Value -> name lookup (enum reverse-map), for payload-limit error labels.
|
|
73
|
+
// Later keys win on duplicate values (11 -> "String"), as TS enums do.
|
|
74
|
+
const PayloadBufferName = {};
|
|
75
|
+
for (const key of Object.keys(PayloadBuffer)) {
|
|
76
|
+
PayloadBufferName[PayloadBuffer[key]] = key;
|
|
77
|
+
}
|
|
78
|
+
export const payloadBufferName = (value) => PayloadBufferName[value] ?? String(value);
|
|
79
|
+
export const LockBound = {
|
|
80
|
+
paddingLock: 0,
|
|
81
|
+
padding: 0,
|
|
82
|
+
slots: 32,
|
|
83
|
+
header: 0,
|
|
84
|
+
};
|
|
73
85
|
export const LOCK_CACHE_LINE_BYTES = 64;
|
|
74
86
|
export const LOCK_SECTOR_BYTES = 256;
|
|
75
87
|
export const PayloadTransportFinalizer = Symbol.for("knitting.payloadCodec.transportFinalizer");
|
|
@@ -123,34 +135,33 @@ export const runTaskFinalizers = (task) => {
|
|
|
123
135
|
}
|
|
124
136
|
}
|
|
125
137
|
};
|
|
126
|
-
export
|
|
127
|
-
(function (TaskIndex) {
|
|
138
|
+
export const TaskIndex = {
|
|
128
139
|
/**
|
|
129
140
|
* Worker -> host response flags word.
|
|
130
141
|
*/
|
|
131
|
-
|
|
142
|
+
FlagsToHost: 0,
|
|
132
143
|
/**
|
|
133
144
|
* Host -> worker request function id (low 16 bits).
|
|
134
145
|
* High 16 bits are reserved for caller metadata on request path.
|
|
135
146
|
* NOTE: shares the same storage word as `FlagsToHost`.
|
|
136
147
|
*/
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
148
|
+
FunctionID: 0,
|
|
149
|
+
ID: 1,
|
|
150
|
+
Type: 2,
|
|
151
|
+
Start: 3,
|
|
152
|
+
End: 4,
|
|
153
|
+
PayloadLen: 5,
|
|
143
154
|
/**
|
|
144
155
|
* Low 5 bits: region slot index (0..31).
|
|
145
156
|
* High 27 bits: reserved for caller metadata (e.g. enqueue timing).
|
|
146
157
|
*/
|
|
147
|
-
|
|
148
|
-
|
|
158
|
+
slotBuffer: 6,
|
|
159
|
+
Size: 8,
|
|
149
160
|
/**
|
|
150
161
|
* Total slot length in Uint32 words, including the task header.
|
|
151
162
|
*/
|
|
152
|
-
|
|
153
|
-
}
|
|
163
|
+
TotalBuff: 144,
|
|
164
|
+
};
|
|
154
165
|
export const TASK_SLOT_INDEX_BITS = 5;
|
|
155
166
|
export const TASK_SLOT_INDEX_MASK = (1 << TASK_SLOT_INDEX_BITS) - 1;
|
|
156
167
|
export const TASK_SLOT_META_BITS = 32 - TASK_SLOT_INDEX_BITS;
|
|
@@ -186,10 +197,9 @@ export const setTaskSlotMeta = (task, value) => {
|
|
|
186
197
|
task[TaskIndex.slotBuffer] =
|
|
187
198
|
((task[TaskIndex.slotBuffer] & TASK_SLOT_INDEX_MASK) | encodedMeta) >>> 0;
|
|
188
199
|
};
|
|
189
|
-
export
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
})(TaskFlag || (TaskFlag = {}));
|
|
200
|
+
export const TaskFlag = {
|
|
201
|
+
Reject: 1,
|
|
202
|
+
};
|
|
193
203
|
// Main queue lock layout in bytes.
|
|
194
204
|
// The queue protocol uses two Int32 signal words, each on its own cache line:
|
|
195
205
|
// - hostBits at byte 0
|
|
@@ -216,6 +226,27 @@ export const HEADER_TASK_LINE_U32 = LOCK_CACHE_LINE_BYTES /
|
|
|
216
226
|
export const HEADER_STATIC_PAYLOAD_U32 = TaskIndex.TotalBuff -
|
|
217
227
|
HEADER_TASK_LINE_U32;
|
|
218
228
|
export const HEADER_TASK_OFFSET_IN_SLOT_U32 = HEADER_STATIC_PAYLOAD_U32;
|
|
229
|
+
// Work-stealing control words live in the unused tail of each slot's header
|
|
230
|
+
// cache line. Task words occupy 0..7 (`TASK_LOCAL_FLAGS_INDEX = 7`) of the
|
|
231
|
+
// 16-word line, leaving words 8..15 free. Consumer `c` owns slot `c`'s pair, and
|
|
232
|
+
// because the slot stride is exactly 9 cache lines those pairs land on distinct
|
|
233
|
+
// lines. `ACK[c]` and `WANT[c]` share one line on purpose: same single writer,
|
|
234
|
+
// and a peer reading one pulls the other for free.
|
|
235
|
+
export const STEAL_ACK_SLOT_OFFSET_U32 = HEADER_TASK_OFFSET_IN_SLOT_U32 +
|
|
236
|
+
TaskIndex.Size;
|
|
237
|
+
export const STEAL_WANT_SLOT_OFFSET_U32 = STEAL_ACK_SLOT_OFFSET_U32 + 1;
|
|
238
|
+
/**
|
|
239
|
+
* The payload allocator needs its own participant-owned ACK per consumer, since
|
|
240
|
+
* several workers can free payload slots from one shared allocator. No intent
|
|
241
|
+
* word: freeing needs no arbitration, a consumer only frees what it drained.
|
|
242
|
+
*/
|
|
243
|
+
export const STEAL_PAYLOAD_ACK_SLOT_OFFSET_U32 = STEAL_ACK_SLOT_OFFSET_U32 + 2;
|
|
244
|
+
/**
|
|
245
|
+
* Host-owned liveness mask for stealing consumers. A dead claimant may leave
|
|
246
|
+
* its WANT word set forever; survivors ignore WANT from consumers whose bit is
|
|
247
|
+
* clear here. This preserves single-writer ownership of every WANT word.
|
|
248
|
+
*/
|
|
249
|
+
export const STEAL_LIVE_SLOT_OFFSET_U32 = STEAL_PAYLOAD_ACK_SLOT_OFFSET_U32 + 1;
|
|
219
250
|
export const HEADER_U32_LENGTH = LockBound.header +
|
|
220
251
|
(HEADER_SLOT_STRIDE_U32 * LockBound.slots);
|
|
221
252
|
export const HEADER_BYTE_LENGTH = HEADER_U32_LENGTH *
|
|
@@ -240,6 +271,8 @@ export const makeTask = () => {
|
|
|
240
271
|
const fillTaskFrom = (task, array, at) => {
|
|
241
272
|
task[0] = array[at];
|
|
242
273
|
task[1] = array[at + 1];
|
|
274
|
+
// Raw numeric tag word; const-object payload "enums" are strict literal unions
|
|
275
|
+
// (numeric `enum` accepted any number), so cast back on restore.
|
|
243
276
|
task[2] = array[at + 2];
|
|
244
277
|
task[3] = array[at + 3];
|
|
245
278
|
task[4] = array[at + 4];
|
|
@@ -264,7 +297,7 @@ const settleTask = (task) => {
|
|
|
264
297
|
task[TaskIndex["FlagsToHost"]] = 0;
|
|
265
298
|
}
|
|
266
299
|
};
|
|
267
|
-
export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload, payloadConfig, payloadSector, textCompat, resultList, toSentList, recycleList, }) => {
|
|
300
|
+
export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload, payloadConfig, payloadSector, textCompat, resultList, toSentList, recycleList, processBoundary, consumers, consumerId, regionLanes, }) => {
|
|
268
301
|
// Layout within `lockSectorRegion`:
|
|
269
302
|
// - hostBits starts at byte 0
|
|
270
303
|
// - workerBits starts at byte 64
|
|
@@ -296,8 +329,62 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
296
329
|
headers: headersRegion,
|
|
297
330
|
payload: payloadSAB,
|
|
298
331
|
});
|
|
332
|
+
// ---- work stealing (multi-consumer) state ----
|
|
333
|
+
// With N consumers the pending set is A ^ ACK[0] ^ ... ^ ACK[N-1]; every word
|
|
334
|
+
// still has exactly one writer, so no shared writable word is introduced.
|
|
335
|
+
const stealConsumers = Math.max(1, (consumers ?? 1) | 0);
|
|
336
|
+
const stealEnabled = stealConsumers > 1;
|
|
337
|
+
// A lock2 built without `consumerId` is the producer endpoint: it encodes but
|
|
338
|
+
// never claims. It still frees payload slots, so it needs an acknowledgement
|
|
339
|
+
// word of its own rather than sharing consumer 0's.
|
|
340
|
+
const stealIsProducer = consumerId === undefined;
|
|
341
|
+
const stealId = (consumerId ?? 0) | 0;
|
|
342
|
+
const stealRegionLanes = (regionLanes ?? 8) | 0;
|
|
343
|
+
const stealRegions = (LockBound.slots / stealRegionLanes) | 0;
|
|
344
|
+
if (stealEnabled) {
|
|
345
|
+
if (stealRegionLanes < 1 || (stealRegionLanes & (stealRegionLanes - 1)) !== 0) {
|
|
346
|
+
throw new RangeError("regionLanes must be a power of two");
|
|
347
|
+
}
|
|
348
|
+
if (stealRegions < stealConsumers) {
|
|
349
|
+
throw new RangeError(`regionLanes=${stealRegionLanes} yields ${stealRegions} regions, ` +
|
|
350
|
+
`too few for ${stealConsumers} consumers`);
|
|
351
|
+
}
|
|
352
|
+
if (stealId < 0 || stealId >= stealConsumers) {
|
|
353
|
+
throw new RangeError(`consumerId ${stealId} out of range`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// Int32 alias over the headers SAB so the control words get signed bit math
|
|
357
|
+
// consistent with the rest of the protocol.
|
|
358
|
+
const stealView = new Int32Array(headersRegion.sab, headersRegion.byteOffset, headersRegion.byteLength >>> 2);
|
|
359
|
+
const stealAckIndex = new Int32Array(stealConsumers);
|
|
360
|
+
const stealWantIndex = new Int32Array(stealConsumers);
|
|
361
|
+
for (let c = 0; c < stealConsumers; c++) {
|
|
362
|
+
const slotBase = (c * headersSlotStride) + LockBound.header;
|
|
363
|
+
stealAckIndex[c] = slotBase + STEAL_ACK_SLOT_OFFSET_U32;
|
|
364
|
+
stealWantIndex[c] = slotBase + STEAL_WANT_SLOT_OFFSET_U32;
|
|
365
|
+
}
|
|
366
|
+
const stealLiveIndex = LockBound.header + STEAL_LIVE_SLOT_OFFSET_U32;
|
|
367
|
+
const stealAllLiveMask = stealConsumers === 32
|
|
368
|
+
? -1
|
|
369
|
+
: ((1 << stealConsumers) - 1) | 0;
|
|
370
|
+
if (stealEnabled && stealIsProducer) {
|
|
371
|
+
Atomics.store(stealView, stealLiveIndex, stealAllLiveMask);
|
|
372
|
+
}
|
|
373
|
+
const stealIsLive = (mask, consumer) => (mask & (1 << consumer)) !== 0;
|
|
374
|
+
const stealAckXorAll = () => {
|
|
375
|
+
let x = 0 | 0;
|
|
376
|
+
for (let c = 0; c < stealConsumers; c++) {
|
|
377
|
+
x = (x ^ a_load(stealView, stealAckIndex[c])) | 0;
|
|
378
|
+
}
|
|
379
|
+
return x;
|
|
380
|
+
};
|
|
299
381
|
let promiseHandler;
|
|
300
|
-
|
|
382
|
+
if (registeredEncodePayload === undefined ||
|
|
383
|
+
registeredDecodePayload === undefined) {
|
|
384
|
+
throw new Error("Payload codec not registered before lock2(). Ensure the module that " +
|
|
385
|
+
'builds locks imports "./payloadCodec.ts" (it self-registers on load).');
|
|
386
|
+
}
|
|
387
|
+
const encodeTask = registeredEncodePayload({
|
|
301
388
|
payload: {
|
|
302
389
|
sab: payloadSAB,
|
|
303
390
|
config: resolvedPayloadConfig,
|
|
@@ -306,6 +393,7 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
306
393
|
headerSlotStrideU32: headersSlotStride,
|
|
307
394
|
lockSector: payloadLockRegion,
|
|
308
395
|
textCompat: resolvedTextCompat,
|
|
396
|
+
processBoundary,
|
|
309
397
|
onPromise: (task, isRejected, value) => {
|
|
310
398
|
if ((task[TASK_LOCAL_FLAGS_INDEX] & TASK_LOCAL_PROMISE_TRACKED_FLAG) !==
|
|
311
399
|
0 &&
|
|
@@ -318,7 +406,7 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
318
406
|
promiseHandler(task, isRejected, value);
|
|
319
407
|
},
|
|
320
408
|
});
|
|
321
|
-
const decodeTask =
|
|
409
|
+
const decodeTask = registeredDecodePayload({
|
|
322
410
|
payload: {
|
|
323
411
|
sab: payloadSAB,
|
|
324
412
|
config: resolvedPayloadConfig,
|
|
@@ -327,6 +415,7 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
327
415
|
headerSlotStrideU32: headersSlotStride,
|
|
328
416
|
lockSector: payloadLockRegion,
|
|
329
417
|
textCompat: resolvedTextCompat,
|
|
418
|
+
processBoundary,
|
|
330
419
|
});
|
|
331
420
|
let LastLocal = 0 | 0;
|
|
332
421
|
let LastWorker = 0 | 0;
|
|
@@ -343,8 +432,11 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
343
432
|
// false-busy-only sender-side staleness property, this may hide newly freed
|
|
344
433
|
// lanes but cannot make a genuinely pending lane appear free. Refresh only
|
|
345
434
|
// when the cached free set is exhausted.
|
|
346
|
-
let workerShadow =
|
|
347
|
-
const refreshWorkerShadow =
|
|
435
|
+
let workerShadow = 0 | 0;
|
|
436
|
+
const refreshWorkerShadow = stealEnabled
|
|
437
|
+
? () => workerShadow = stealAckXorAll()
|
|
438
|
+
: () => workerShadow = a_load(workerBits, 0) | 0;
|
|
439
|
+
refreshWorkerShadow();
|
|
348
440
|
const ensureSenderStateHasFree = (state) => (~state) !== 0 ? state : (LastLocal ^ refreshWorkerShadow()) | 0;
|
|
349
441
|
// RingQueue method aliases (hot path)
|
|
350
442
|
const toBeSentPush = (task) => toBeSent.push(task);
|
|
@@ -542,6 +634,122 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
542
634
|
lastTake = last;
|
|
543
635
|
return true;
|
|
544
636
|
};
|
|
637
|
+
/**
|
|
638
|
+
* WORKER SIDE: decode, region-Dekker stealing variant.
|
|
639
|
+
*
|
|
640
|
+
* Claims one whole region of `stealRegionLanes` lanes per handshake, so the
|
|
641
|
+
* StoreLoad barrier is amortised over the region rather than paid per task.
|
|
642
|
+
* Priority is fixed by endpoint id: a junior withdraws for a senior, so at
|
|
643
|
+
* most one consumer ever acknowledges a published generation.
|
|
644
|
+
*
|
|
645
|
+
* The C reference needs an explicit mfence between the intent store and the
|
|
646
|
+
* peer loads. `Atomics.store` is sequentially consistent in JS, so that
|
|
647
|
+
* barrier is implicit here and holds on x86 and ARM alike.
|
|
648
|
+
*/
|
|
649
|
+
const stealLaneMask = (region) => stealRegionLanes === 32
|
|
650
|
+
? -1
|
|
651
|
+
: ((((1 << stealRegionLanes) - 1) << (region * stealRegionLanes)) | 0);
|
|
652
|
+
const stealPeerAcks = () => {
|
|
653
|
+
let x = 0 | 0;
|
|
654
|
+
for (let c = 0; c < stealConsumers; c++) {
|
|
655
|
+
if (c !== stealId)
|
|
656
|
+
x = (x ^ a_load(stealView, stealAckIndex[c])) | 0;
|
|
657
|
+
}
|
|
658
|
+
return x;
|
|
659
|
+
};
|
|
660
|
+
const stealJuniorWants = (intent) => {
|
|
661
|
+
const live = a_load(stealView, stealLiveIndex) | 0;
|
|
662
|
+
for (let c = stealId + 1; c < stealConsumers; c++) {
|
|
663
|
+
if (!stealIsLive(live, c))
|
|
664
|
+
continue;
|
|
665
|
+
if ((a_load(stealView, stealWantIndex[c]) & intent) !== 0)
|
|
666
|
+
return true;
|
|
667
|
+
}
|
|
668
|
+
return false;
|
|
669
|
+
};
|
|
670
|
+
let stealCursor = ((stealRegions * stealId) / stealConsumers) | 0;
|
|
671
|
+
const decodeSteal = () => {
|
|
672
|
+
const pending = (a_load(hostBits, 0) ^ LastWorker ^ stealPeerAcks()) | 0;
|
|
673
|
+
if (pending === 0)
|
|
674
|
+
return false;
|
|
675
|
+
let pendingRegions = 0 | 0;
|
|
676
|
+
if (stealRegions === 1)
|
|
677
|
+
pendingRegions = 1;
|
|
678
|
+
else {
|
|
679
|
+
for (let r = 0; r < stealRegions; r++) {
|
|
680
|
+
if ((pending & stealLaneMask(r)) !== 0)
|
|
681
|
+
pendingRegions |= 1 << r;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
const liveBeforeClaim = a_load(stealView, stealLiveIndex) | 0;
|
|
685
|
+
let peerIntent = 0 | 0;
|
|
686
|
+
let seniorIntent = 0 | 0;
|
|
687
|
+
for (let c = 0; c < stealConsumers; c++) {
|
|
688
|
+
if (c === stealId || !stealIsLive(liveBeforeClaim, c))
|
|
689
|
+
continue;
|
|
690
|
+
const value = a_load(stealView, stealWantIndex[c]) | 0;
|
|
691
|
+
peerIntent |= value;
|
|
692
|
+
if (c < stealId)
|
|
693
|
+
seniorIntent |= value;
|
|
694
|
+
}
|
|
695
|
+
const notSenior = pendingRegions & ~seniorIntent;
|
|
696
|
+
if (notSenior === 0)
|
|
697
|
+
return false;
|
|
698
|
+
const clean = pendingRegions & ~peerIntent;
|
|
699
|
+
const candidates = clean !== 0 ? clean : notSenior;
|
|
700
|
+
let region = -1;
|
|
701
|
+
for (let step = 0; step < stealRegions; step++) {
|
|
702
|
+
const candidate = (stealCursor + step) % stealRegions;
|
|
703
|
+
if ((candidates & (1 << candidate)) !== 0) {
|
|
704
|
+
region = candidate;
|
|
705
|
+
break;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
if (region < 0)
|
|
709
|
+
return false;
|
|
710
|
+
const intent = (1 << region) | 0;
|
|
711
|
+
a_store(stealView, stealWantIndex[stealId], intent);
|
|
712
|
+
// Dekker StoreLoad: implicit, `Atomics.store` above is seq-cst.
|
|
713
|
+
let seniorConflict = false;
|
|
714
|
+
let juniorConflict = false;
|
|
715
|
+
for (let c = 0; c < stealConsumers; c++) {
|
|
716
|
+
if (c === stealId || !stealIsLive(liveBeforeClaim, c))
|
|
717
|
+
continue;
|
|
718
|
+
if ((a_load(stealView, stealWantIndex[c]) & intent) === 0)
|
|
719
|
+
continue;
|
|
720
|
+
if (c < stealId)
|
|
721
|
+
seniorConflict = true;
|
|
722
|
+
else
|
|
723
|
+
juniorConflict = true;
|
|
724
|
+
}
|
|
725
|
+
if (seniorConflict) {
|
|
726
|
+
a_store(stealView, stealWantIndex[stealId], 0);
|
|
727
|
+
return false;
|
|
728
|
+
}
|
|
729
|
+
if (juniorConflict) {
|
|
730
|
+
while (stealJuniorWants(intent)) { /* junior withdraws */ }
|
|
731
|
+
}
|
|
732
|
+
// Last control loads before ownership. After this no control word is read
|
|
733
|
+
// until every slot in `take` has been decoded.
|
|
734
|
+
const take = ((a_load(hostBits, 0) ^ LastWorker ^ stealPeerAcks()) &
|
|
735
|
+
stealLaneMask(region)) | 0;
|
|
736
|
+
if (take === 0) {
|
|
737
|
+
a_store(stealView, stealWantIndex[stealId], 0);
|
|
738
|
+
return false;
|
|
739
|
+
}
|
|
740
|
+
let lanes = take;
|
|
741
|
+
while (lanes !== 0) {
|
|
742
|
+
decodeAt(31 - clz32((lanes & -lanes) >>> 0));
|
|
743
|
+
lanes = (lanes & (lanes - 1)) | 0;
|
|
744
|
+
}
|
|
745
|
+
// Retire the whole claimed mask with one ACK store, then release the region.
|
|
746
|
+
// ACK before intent-clear (paper Assumption 2(ii)).
|
|
747
|
+
LastWorker = (LastWorker ^ take) | 0;
|
|
748
|
+
a_store(stealView, stealAckIndex[stealId], LastWorker);
|
|
749
|
+
a_store(stealView, stealWantIndex[stealId], 0);
|
|
750
|
+
stealCursor = (region + 1) % stealRegions;
|
|
751
|
+
return true;
|
|
752
|
+
};
|
|
545
753
|
/**
|
|
546
754
|
* HOST SIDE: decode version
|
|
547
755
|
*/
|
|
@@ -711,6 +919,24 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
711
919
|
deferredCount = 0 | 0;
|
|
712
920
|
pendingPromiseCount = 0 | 0;
|
|
713
921
|
};
|
|
922
|
+
/**
|
|
923
|
+
* HOST SIDE: remove a terminated consumer from stealing arbitration.
|
|
924
|
+
*
|
|
925
|
+
* Its ACK remains part of the generation XOR forever, because it records
|
|
926
|
+
* lanes that consumer retired while alive. Only its stale WANT becomes
|
|
927
|
+
* ineligible. The caller must invoke this only after the endpoint can no
|
|
928
|
+
* longer execute; there is deliberately no matching reactivation operation.
|
|
929
|
+
*/
|
|
930
|
+
const deactivateStealConsumer = (id) => {
|
|
931
|
+
if (!stealEnabled || !stealIsProducer)
|
|
932
|
+
return false;
|
|
933
|
+
if (!Number.isInteger(id) || id < 0 || id >= stealConsumers) {
|
|
934
|
+
throw new RangeError(`consumerId ${id} out of range`);
|
|
935
|
+
}
|
|
936
|
+
const bit = 1 << id;
|
|
937
|
+
const previous = Atomics.and(stealView, stealLiveIndex, ~bit);
|
|
938
|
+
return (previous & bit) !== 0;
|
|
939
|
+
};
|
|
714
940
|
return {
|
|
715
941
|
enlist,
|
|
716
942
|
encode,
|
|
@@ -718,7 +944,7 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
718
944
|
encodeAll,
|
|
719
945
|
publish,
|
|
720
946
|
flushPending,
|
|
721
|
-
decode,
|
|
947
|
+
decode: stealEnabled ? decodeSteal : decode,
|
|
722
948
|
hasSpace,
|
|
723
949
|
resolved,
|
|
724
950
|
hostBits,
|
|
@@ -729,6 +955,7 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
729
955
|
getPendingFrameCount: () => toBeSent.size | 0,
|
|
730
956
|
getPendingPromiseCount: () => pendingPromiseCount | 0,
|
|
731
957
|
resetPendingState,
|
|
958
|
+
deactivateStealConsumer,
|
|
732
959
|
takeDeferredCount: () => {
|
|
733
960
|
const count = deferredCount | 0;
|
|
734
961
|
deferredCount = 0 | 0;
|
|
@@ -6,7 +6,7 @@ import type { SharedBufferSource } from "../common/shared-buffer-region.js";
|
|
|
6
6
|
* Returns `true` when the payload is encoded successfully.
|
|
7
7
|
* Returns `false` when dynamic payload space could not be reserved.
|
|
8
8
|
*/
|
|
9
|
-
export declare const encodePayload: ({ lockSector, payload, sab, payloadConfig, headersBuffer, headerSlotStrideU32, textCompat, onPromise, }: {
|
|
9
|
+
export declare const encodePayload: ({ lockSector, payload, sab, payloadConfig, headersBuffer, headerSlotStrideU32, textCompat, onPromise, processBoundary, }: {
|
|
10
10
|
lockSector?: SharedBufferSource;
|
|
11
11
|
payload?: {
|
|
12
12
|
sab?: SharedBufferSource;
|
|
@@ -24,8 +24,13 @@ export declare const encodePayload: ({ lockSector, payload, sab, payloadConfig,
|
|
|
24
24
|
headerSlotStrideU32?: number;
|
|
25
25
|
textCompat?: LockBufferTextCompat;
|
|
26
26
|
onPromise?: PromisePayloadHandler;
|
|
27
|
+
/**
|
|
28
|
+
* Reject process-local pointer payloads while preserving ProcessSharedBuffer.
|
|
29
|
+
* Process transports must set this on both encoders and decoders.
|
|
30
|
+
*/
|
|
31
|
+
processBoundary?: boolean;
|
|
27
32
|
}) => (task: Task, slotIndex: number) => boolean;
|
|
28
|
-
export declare const decodePayload: ({ lockSector, payload, sab, payloadConfig, headersBuffer, headerSlotStrideU32, textCompat, host, }: {
|
|
33
|
+
export declare const decodePayload: ({ lockSector, payload, sab, payloadConfig, headersBuffer, headerSlotStrideU32, textCompat, host, processBoundary, }: {
|
|
29
34
|
lockSector?: SharedBufferSource;
|
|
30
35
|
payload?: {
|
|
31
36
|
sab?: SharedBufferSource;
|
|
@@ -43,4 +48,8 @@ export declare const decodePayload: ({ lockSector, payload, sab, payloadConfig,
|
|
|
43
48
|
headerSlotStrideU32?: number;
|
|
44
49
|
textCompat?: LockBufferTextCompat;
|
|
45
50
|
host?: true;
|
|
51
|
+
/**
|
|
52
|
+
* Reject process-local pointer payloads before reading pointer/token metadata.
|
|
53
|
+
*/
|
|
54
|
+
processBoundary?: boolean;
|
|
46
55
|
}) => (task: Task, slotIndex: number, specialFlags?: number) => void;
|