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
package/src/memory/lock.js
CHANGED
|
@@ -9,12 +9,7 @@ import { createSharedArrayBuffer, createWasmSharedArrayBuffer, } from "../common
|
|
|
9
9
|
import { toSharedBufferRegion, } from "../common/shared-buffer-region.js";
|
|
10
10
|
import { probeLockBufferTextCompat, } from "../common/shared-buffer-text.js";
|
|
11
11
|
import { resolvePayloadBufferOptions, } from "./payload-config.js";
|
|
12
|
-
|
|
13
|
-
* TODO: Compose all the instance where the array is passed as argument
|
|
14
|
-
*/
|
|
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.
|
|
12
|
+
// Use const objects instead of enums; Andromeda's Nova engine cannot parse enums.
|
|
18
13
|
export const PayloadSignal = {
|
|
19
14
|
UNREACHABLE: 0,
|
|
20
15
|
BigInt: 2,
|
|
@@ -68,6 +63,12 @@ export const PayloadBuffer = {
|
|
|
68
63
|
EnvelopeDynamicHeaderStringExternal: 52,
|
|
69
64
|
NumericArray: 53,
|
|
70
65
|
StaticNumericArray: 54,
|
|
66
|
+
/** Binary payload already stored in the dynamic arena. */
|
|
67
|
+
ArenaBinary: 55,
|
|
68
|
+
/** A returned Uint8Array whose ArrayBuffer ownership was moved to the host. */
|
|
69
|
+
MovedBinary: 56,
|
|
70
|
+
/** A returned ArrayBuffer whose ownership was moved to the host. */
|
|
71
|
+
MovedArrayBuffer: 57,
|
|
71
72
|
};
|
|
72
73
|
// Value -> name lookup (enum reverse-map), for payload-limit error labels.
|
|
73
74
|
// Later keys win on duplicate values (11 -> "String"), as TS enums do.
|
|
@@ -116,6 +117,13 @@ export const addTaskFinalizer = (task, finalizer) => {
|
|
|
116
117
|
}
|
|
117
118
|
};
|
|
118
119
|
};
|
|
120
|
+
/** Take an owner-lifetime transport hold, if the value supports one. */
|
|
121
|
+
export const takePayloadTransportHold = (value) => {
|
|
122
|
+
if (value === null || typeof value !== "object")
|
|
123
|
+
return undefined;
|
|
124
|
+
const finalizer = value[PayloadTransportFinalizer]?.();
|
|
125
|
+
return typeof finalizer === "function" ? finalizer : undefined;
|
|
126
|
+
};
|
|
119
127
|
export const attachPayloadTransportFinalizer = (task, value) => {
|
|
120
128
|
if (task.finalize !== undefined || value === null || typeof value !== "object") {
|
|
121
129
|
return;
|
|
@@ -143,18 +151,19 @@ export const TaskIndex = {
|
|
|
143
151
|
/**
|
|
144
152
|
* Host -> worker request function id (low 16 bits).
|
|
145
153
|
* High 16 bits are reserved for caller metadata on request path.
|
|
146
|
-
*
|
|
154
|
+
* Shares storage with `FlagsToHost`.
|
|
147
155
|
*/
|
|
148
156
|
FunctionID: 0,
|
|
149
157
|
ID: 1,
|
|
150
158
|
Type: 2,
|
|
151
159
|
Start: 3,
|
|
152
|
-
End: 4,
|
|
153
|
-
PayloadLen: 5,
|
|
154
160
|
/**
|
|
155
|
-
*
|
|
156
|
-
*
|
|
161
|
+
* Payload length; bit 31 stores the high bit of a dynamic-region index.
|
|
162
|
+
* Write the length before `tagTaskSlot()` and mask bit 31 when reading it.
|
|
157
163
|
*/
|
|
164
|
+
End: 4,
|
|
165
|
+
PayloadLen: 5,
|
|
166
|
+
/** Low 5 bits hold the dynamic-region index; the remaining bits hold metadata. */
|
|
158
167
|
slotBuffer: 6,
|
|
159
168
|
Size: 8,
|
|
160
169
|
/**
|
|
@@ -228,10 +237,9 @@ export const HEADER_STATIC_PAYLOAD_U32 = TaskIndex.TotalBuff -
|
|
|
228
237
|
export const HEADER_TASK_OFFSET_IN_SLOT_U32 = HEADER_STATIC_PAYLOAD_U32;
|
|
229
238
|
// Work-stealing control words live in the unused tail of each slot's header
|
|
230
239
|
// 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
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
// and a peer reading one pulls the other for free.
|
|
240
|
+
// 16-word line, leaving words 8..15 free. Consumer `c` owns slot `c`'s words,
|
|
241
|
+
// and the slot stride keeps those words on separate cache lines.
|
|
242
|
+
// The ACK word was removed; retirement is folded into `workerBits`.
|
|
235
243
|
export const STEAL_ACK_SLOT_OFFSET_U32 = HEADER_TASK_OFFSET_IN_SLOT_U32 +
|
|
236
244
|
TaskIndex.Size;
|
|
237
245
|
export const STEAL_WANT_SLOT_OFFSET_U32 = STEAL_ACK_SLOT_OFFSET_U32 + 1;
|
|
@@ -249,6 +257,13 @@ export const STEAL_PAYLOAD_ACK_SLOT_OFFSET_U32 = STEAL_ACK_SLOT_OFFSET_U32 + 2;
|
|
|
249
257
|
export const STEAL_LIVE_SLOT_OFFSET_U32 = STEAL_PAYLOAD_ACK_SLOT_OFFSET_U32 + 1;
|
|
250
258
|
/** Host-owned arm word for the return-lock completion doorbell. */
|
|
251
259
|
export const DOORBELL_ARMED_SLOT_OFFSET_U32 = STEAL_LIVE_SLOT_OFFSET_U32 + 1;
|
|
260
|
+
/** Shared region-owner mask for the `cas-mask` claim discipline. */
|
|
261
|
+
export const STEAL_CLAIM_MASK_SLOT_OFFSET_U32 = DOORBELL_ARMED_SLOT_OFFSET_U32 + 1;
|
|
262
|
+
// A producer claims ARMED before ringing the host. Keeping SIGNALLED until the
|
|
263
|
+
// host drains coalesces a burst of published frames into one native/IPC wake.
|
|
264
|
+
const DOORBELL_OFF = 0;
|
|
265
|
+
const DOORBELL_ARMED = 1;
|
|
266
|
+
const DOORBELL_SIGNALLED = 2;
|
|
252
267
|
export const HEADER_U32_LENGTH = LockBound.header +
|
|
253
268
|
(HEADER_SLOT_STRIDE_U32 * LockBound.slots);
|
|
254
269
|
export const HEADER_BYTE_LENGTH = HEADER_U32_LENGTH *
|
|
@@ -288,18 +303,17 @@ const makeTaskFrom = (array, at) => {
|
|
|
288
303
|
fillTaskFrom(task, array, at);
|
|
289
304
|
return task;
|
|
290
305
|
};
|
|
291
|
-
// could be inlined
|
|
292
306
|
const settleTask = (task) => {
|
|
293
307
|
if (task[TaskIndex["FlagsToHost"]] === 0) {
|
|
294
308
|
task.resolve(task.value);
|
|
295
309
|
}
|
|
296
310
|
else {
|
|
297
311
|
task.reject(task.value);
|
|
298
|
-
//
|
|
312
|
+
// Clear the rejection marker before the task slot is reused.
|
|
299
313
|
task[TaskIndex["FlagsToHost"]] = 0;
|
|
300
314
|
}
|
|
301
315
|
};
|
|
302
|
-
export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload, payloadConfig, payloadSector, textCompat, resultList, toSentList, recycleList, processBoundary, consumers, consumerId, regionLanes, notifyOnHostPublish, }) => {
|
|
316
|
+
export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload, payloadConfig, payloadSector, textCompat, resultList, toSentList, recycleList, processBoundary, sharedReturn, moveReturn, consumers, consumerId, regionLanes, stealClaim, notifyOnHostPublish, notifyHostPublish, }) => {
|
|
303
317
|
// Layout within `lockSectorRegion`:
|
|
304
318
|
// - hostBits starts at byte 0
|
|
305
319
|
// - workerBits starts at byte 64
|
|
@@ -307,9 +321,8 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
307
321
|
// The remaining two cache lines in the 256-byte sector are reserved for the
|
|
308
322
|
// payload allocator lock (`PAYLOAD_LOCK_*` at bytes 128 and 192).
|
|
309
323
|
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
// the "return lock" (worker->host responses) still publishes into `hostBits`.
|
|
324
|
+
// encode toggles hostBits; decode/resolveHost toggles workerBits. The return
|
|
325
|
+
// lock therefore publishes responses through hostBits as well.
|
|
313
326
|
const lockSectorRegion = toSharedBufferRegion(LockBoundSector ??
|
|
314
327
|
createWasmSharedArrayBuffer(LOCK_SECTOR_BYTE_LENGTH));
|
|
315
328
|
const LockBoundSAB = lockSectorRegion.sab;
|
|
@@ -336,23 +349,22 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
336
349
|
headers: headersRegion,
|
|
337
350
|
payload: payloadSAB,
|
|
338
351
|
});
|
|
339
|
-
//
|
|
340
|
-
// With N consumers the pending set is A ^ ACK[0] ^ ... ^ ACK[N-1]; every word
|
|
341
|
-
// still has exactly one writer, so no shared writable word is introduced.
|
|
352
|
+
// Work-stealing state. Pending lanes are A ^ ACK[0] ^ ... ^ ACK[N-1].
|
|
342
353
|
const stealConsumers = Math.max(1, (consumers ?? 1) | 0);
|
|
343
354
|
const stealEnabled = stealConsumers > 1;
|
|
344
|
-
//
|
|
345
|
-
// never claims. It still frees payload slots, so it needs an acknowledgement
|
|
346
|
-
// word of its own rather than sharing consumer 0's.
|
|
355
|
+
// The producer encodes and frees payload slots but never claims a region.
|
|
347
356
|
const stealIsProducer = consumerId === undefined;
|
|
348
357
|
const stealId = (consumerId ?? 0) | 0;
|
|
349
358
|
const stealRegionLanes = (regionLanes ?? 8) | 0;
|
|
350
359
|
const stealRegions = (LockBound.slots / stealRegionLanes) | 0;
|
|
360
|
+
const stealClaimMask = stealClaim === "cas-mask";
|
|
351
361
|
if (stealEnabled) {
|
|
352
362
|
if (stealRegionLanes < 1 || (stealRegionLanes & (stealRegionLanes - 1)) !== 0) {
|
|
353
363
|
throw new RangeError("regionLanes must be a power of two");
|
|
354
364
|
}
|
|
355
|
-
|
|
365
|
+
// Dekker requires at least one region per live consumer plus a spare.
|
|
366
|
+
// CAS-mask claimants can retry when all regions are owned.
|
|
367
|
+
if (!stealClaimMask && stealRegions < stealConsumers) {
|
|
356
368
|
throw new RangeError(`regionLanes=${stealRegionLanes} yields ${stealRegions} regions, ` +
|
|
357
369
|
`too few for ${stealConsumers} consumers`);
|
|
358
370
|
}
|
|
@@ -363,28 +375,24 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
363
375
|
// Int32 alias over the headers SAB so the control words get signed bit math
|
|
364
376
|
// consistent with the rest of the protocol.
|
|
365
377
|
const stealView = new Int32Array(headersRegion.sab, headersRegion.byteOffset, headersRegion.byteLength >>> 2);
|
|
366
|
-
const stealAckIndex = new Int32Array(stealConsumers);
|
|
367
378
|
const stealWantIndex = new Int32Array(stealConsumers);
|
|
368
379
|
for (let c = 0; c < stealConsumers; c++) {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
stealWantIndex[c] = slotBase + STEAL_WANT_SLOT_OFFSET_U32;
|
|
380
|
+
stealWantIndex[c] = (c * headersSlotStride) + LockBound.header +
|
|
381
|
+
STEAL_WANT_SLOT_OFFSET_U32;
|
|
372
382
|
}
|
|
373
383
|
const stealLiveIndex = LockBound.header + STEAL_LIVE_SLOT_OFFSET_U32;
|
|
384
|
+
/** Single shared owner bitmask, used by `cas-mask` only. */
|
|
385
|
+
const stealMaskIndex = LockBound.header + STEAL_CLAIM_MASK_SLOT_OFFSET_U32;
|
|
374
386
|
const stealAllLiveMask = stealConsumers === 32
|
|
375
387
|
? -1
|
|
376
388
|
: ((1 << stealConsumers) - 1) | 0;
|
|
377
389
|
if (stealEnabled && stealIsProducer) {
|
|
378
390
|
Atomics.store(stealView, stealLiveIndex, stealAllLiveMask);
|
|
391
|
+
// A carpet can outlive one pool on a reused buffer; never inherit owners.
|
|
392
|
+
if (stealClaimMask)
|
|
393
|
+
Atomics.store(stealView, stealMaskIndex, 0);
|
|
379
394
|
}
|
|
380
395
|
const stealIsLive = (mask, consumer) => (mask & (1 << consumer)) !== 0;
|
|
381
|
-
const stealAckXorAll = () => {
|
|
382
|
-
let x = 0 | 0;
|
|
383
|
-
for (let c = 0; c < stealConsumers; c++) {
|
|
384
|
-
x = (x ^ a_load(stealView, stealAckIndex[c])) | 0;
|
|
385
|
-
}
|
|
386
|
-
return x;
|
|
387
|
-
};
|
|
388
396
|
let promiseHandler;
|
|
389
397
|
if (registeredEncodePayload === undefined ||
|
|
390
398
|
registeredDecodePayload === undefined) {
|
|
@@ -401,6 +409,8 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
401
409
|
lockSector: payloadLockRegion,
|
|
402
410
|
textCompat: resolvedTextCompat,
|
|
403
411
|
processBoundary,
|
|
412
|
+
sharedReturn,
|
|
413
|
+
moveReturn,
|
|
404
414
|
onPromise: (task, isRejected, value) => {
|
|
405
415
|
if ((task[TASK_LOCAL_FLAGS_INDEX] & TASK_LOCAL_PROMISE_TRACKED_FLAG) !==
|
|
406
416
|
0 &&
|
|
@@ -432,9 +442,9 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
432
442
|
const resolved = resultList ?? new RingQueue();
|
|
433
443
|
let deferredCount = 0 | 0;
|
|
434
444
|
let pendingPromiseCount = 0 | 0;
|
|
435
|
-
// Atomics aliases (hot path)
|
|
436
445
|
const a_load = Atomics.load;
|
|
437
446
|
const a_store = Atomics.store;
|
|
447
|
+
const a_compareExchange = Atomics.compareExchange;
|
|
438
448
|
const a_notify = Atomics.notify;
|
|
439
449
|
const shouldNotifyHostPublish = notifyOnHostPublish === true;
|
|
440
450
|
const a_waitAsync = typeof Atomics.waitAsync === "function"
|
|
@@ -442,15 +452,13 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
442
452
|
: undefined;
|
|
443
453
|
// Sender-side cached shadow of the receiver-owned queue word. Under the XSC
|
|
444
454
|
// false-busy-only sender-side staleness property, this may hide newly freed
|
|
445
|
-
// lanes but cannot make a
|
|
455
|
+
// lanes but cannot make a pending lane appear free. Refresh only
|
|
446
456
|
// when the cached free set is exhausted.
|
|
447
457
|
let workerShadow = 0 | 0;
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
: () => workerShadow = a_load(workerBits, 0) | 0;
|
|
458
|
+
// Under stealing, workerBits also records retired lanes.
|
|
459
|
+
const refreshWorkerShadow = () => workerShadow = a_load(workerBits, 0) | 0;
|
|
451
460
|
refreshWorkerShadow();
|
|
452
461
|
const ensureSenderStateHasFree = (state) => (~state) !== 0 ? state : (LastLocal ^ refreshWorkerShadow()) | 0;
|
|
453
|
-
// RingQueue method aliases (hot path)
|
|
454
462
|
const toBeSentPush = (task) => toBeSent.push(task);
|
|
455
463
|
const toBeSentShift = () => toBeSent.shiftNoClear();
|
|
456
464
|
const toBeSentUnshift = (task) => toBeSent.unshift(task);
|
|
@@ -574,8 +582,22 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
574
582
|
};
|
|
575
583
|
const storeHost = (bit) => {
|
|
576
584
|
a_store(hostBits, 0, LastLocal = (LastLocal ^ bit) | 0);
|
|
577
|
-
if (shouldNotifyHostPublish &&
|
|
578
|
-
|
|
585
|
+
if (shouldNotifyHostPublish &&
|
|
586
|
+
a_compareExchange(doorbellArmed, 0, DOORBELL_ARMED, DOORBELL_SIGNALLED) === DOORBELL_ARMED) {
|
|
587
|
+
if (notifyHostPublish !== undefined) {
|
|
588
|
+
try {
|
|
589
|
+
notifyHostPublish();
|
|
590
|
+
}
|
|
591
|
+
catch {
|
|
592
|
+
// A callback can be unavailable after forced worker teardown. Restore
|
|
593
|
+
// the arm only when it still belongs to this publication, so a later
|
|
594
|
+
// result can retry without overwriting a host-side disarm/re-arm.
|
|
595
|
+
a_compareExchange(doorbellArmed, 0, DOORBELL_SIGNALLED, DOORBELL_ARMED);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
a_notify(hostBits, 0, 1);
|
|
600
|
+
}
|
|
579
601
|
}
|
|
580
602
|
};
|
|
581
603
|
const storeWorker = (bit) => a_store(workerBits, 0, LastWorker = (LastWorker ^ bit) | 0);
|
|
@@ -618,9 +640,6 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
618
640
|
return true;
|
|
619
641
|
};
|
|
620
642
|
const hasSpace = () => (hostBits[0] ^ LastWorker) !== 0;
|
|
621
|
-
/**
|
|
622
|
-
* WORKER SIDE: decode
|
|
623
|
-
*/
|
|
624
643
|
const decode = () => {
|
|
625
644
|
let diff = (a_load(hostBits, 0) ^ LastWorker) | 0;
|
|
626
645
|
if (diff === 0)
|
|
@@ -651,28 +670,20 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
651
670
|
lastTake = last;
|
|
652
671
|
return true;
|
|
653
672
|
};
|
|
654
|
-
/**
|
|
655
|
-
* WORKER SIDE: decode, region-Dekker stealing variant.
|
|
656
|
-
*
|
|
657
|
-
* Claims one whole region of `stealRegionLanes` lanes per handshake, so the
|
|
658
|
-
* StoreLoad barrier is amortised over the region rather than paid per task.
|
|
659
|
-
* Priority is fixed by endpoint id: a junior withdraws for a senior, so at
|
|
660
|
-
* most one consumer ever acknowledges a published generation.
|
|
661
|
-
*
|
|
662
|
-
* The C reference needs an explicit mfence between the intent store and the
|
|
663
|
-
* peer loads. `Atomics.store` is sequentially consistent in JS, so that
|
|
664
|
-
* barrier is implicit here and holds on x86 and ARM alike.
|
|
665
|
-
*/
|
|
666
673
|
const stealLaneMask = (region) => stealRegionLanes === 32
|
|
667
674
|
? -1
|
|
668
675
|
: ((((1 << stealRegionLanes) - 1) << (region * stealRegionLanes)) | 0);
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
+
const stealRegionBits = stealRegionLanes === 32
|
|
677
|
+
? -1
|
|
678
|
+
: ((1 << stealRegionLanes) - 1) | 0;
|
|
679
|
+
/** Find the oldest pending lane in a claimed region. */
|
|
680
|
+
const stealOldestLane = (region, take) => {
|
|
681
|
+
const low = (region * stealRegionLanes) | 0;
|
|
682
|
+
const lanes = ((take >>> low) & stealRegionBits) | 0;
|
|
683
|
+
const predecessors = ((lanes >>> 1) | (lanes << (stealRegionLanes - 1))) & stealRegionBits;
|
|
684
|
+
const oldest = (lanes & ~predecessors) | 0;
|
|
685
|
+
return low +
|
|
686
|
+
(oldest === 0 ? stealRegionLanes - 1 : 31 - clz32(oldest >>> 0));
|
|
676
687
|
};
|
|
677
688
|
const stealJuniorWants = (intent) => {
|
|
678
689
|
const live = a_load(stealView, stealLiveIndex) | 0;
|
|
@@ -684,9 +695,12 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
684
695
|
}
|
|
685
696
|
return false;
|
|
686
697
|
};
|
|
687
|
-
|
|
698
|
+
const stealHome = ((stealRegions * stealId) / stealConsumers) | 0;
|
|
699
|
+
let stealCursor = stealHome;
|
|
700
|
+
/** Decode one region using the Dekker claim protocol. */
|
|
688
701
|
const decodeSteal = () => {
|
|
689
|
-
|
|
702
|
+
// workerBits includes retired lanes under stealing.
|
|
703
|
+
const pending = (a_load(hostBits, 0) ^ a_load(workerBits, 0)) | 0;
|
|
690
704
|
if (pending === 0)
|
|
691
705
|
return false;
|
|
692
706
|
let pendingRegions = 0 | 0;
|
|
@@ -699,6 +713,8 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
699
713
|
}
|
|
700
714
|
}
|
|
701
715
|
const liveBeforeClaim = a_load(stealView, stealLiveIndex) | 0;
|
|
716
|
+
// Prefer regions no live peer currently wants; the handshake below provides
|
|
717
|
+
// the actual mutual exclusion.
|
|
702
718
|
let peerIntent = 0 | 0;
|
|
703
719
|
let seniorIntent = 0 | 0;
|
|
704
720
|
for (let c = 0; c < stealConsumers; c++) {
|
|
@@ -726,50 +742,138 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
726
742
|
return false;
|
|
727
743
|
const intent = (1 << region) | 0;
|
|
728
744
|
a_store(stealView, stealWantIndex[stealId], intent);
|
|
729
|
-
//
|
|
745
|
+
// Atomics.store is sequentially consistent, so it supplies the StoreLoad
|
|
746
|
+
// barrier required by Dekker.
|
|
747
|
+
// A senior claimant has priority.
|
|
730
748
|
let seniorConflict = false;
|
|
731
|
-
let
|
|
732
|
-
|
|
733
|
-
if (c === stealId || !stealIsLive(liveBeforeClaim, c))
|
|
749
|
+
for (let c = 0; c < stealId; c++) {
|
|
750
|
+
if (!stealIsLive(liveBeforeClaim, c))
|
|
734
751
|
continue;
|
|
735
|
-
if ((a_load(stealView, stealWantIndex[c]) & intent)
|
|
736
|
-
continue;
|
|
737
|
-
if (c < stealId)
|
|
752
|
+
if ((a_load(stealView, stealWantIndex[c]) & intent) !== 0) {
|
|
738
753
|
seniorConflict = true;
|
|
739
|
-
|
|
740
|
-
|
|
754
|
+
break;
|
|
755
|
+
}
|
|
741
756
|
}
|
|
742
757
|
if (seniorConflict) {
|
|
743
758
|
a_store(stealView, stealWantIndex[stealId], 0);
|
|
744
759
|
return false;
|
|
745
760
|
}
|
|
761
|
+
let juniorConflict = false;
|
|
762
|
+
for (let c = stealId + 1; c < stealConsumers; c++) {
|
|
763
|
+
if (!stealIsLive(liveBeforeClaim, c))
|
|
764
|
+
continue;
|
|
765
|
+
if ((a_load(stealView, stealWantIndex[c]) & intent) !== 0) {
|
|
766
|
+
juniorConflict = true;
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
746
770
|
if (juniorConflict) {
|
|
747
771
|
while (stealJuniorWants(intent)) { /* junior withdraws */ }
|
|
748
772
|
}
|
|
749
|
-
//
|
|
750
|
-
|
|
751
|
-
const take = ((a_load(hostBits, 0) ^ LastWorker ^ stealPeerAcks()) &
|
|
773
|
+
// No control word is read again until all selected slots are decoded.
|
|
774
|
+
const take = ((a_load(hostBits, 0) ^ a_load(workerBits, 0)) &
|
|
752
775
|
stealLaneMask(region)) | 0;
|
|
753
776
|
if (take === 0) {
|
|
754
777
|
a_store(stealView, stealWantIndex[stealId], 0);
|
|
755
778
|
return false;
|
|
756
779
|
}
|
|
780
|
+
// Retire lanes and release the claim even if decoding throws.
|
|
757
781
|
let lanes = take;
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
782
|
+
let done = 0 | 0;
|
|
783
|
+
// Decode the claimed lanes in producer order.
|
|
784
|
+
let at = stealOldestLane(region, take);
|
|
785
|
+
const regionLow = (region * stealRegionLanes) | 0;
|
|
786
|
+
const regionHigh = (regionLow + stealRegionLanes - 1) | 0;
|
|
787
|
+
try {
|
|
788
|
+
for (let step = 0; step < stealRegionLanes; step++) {
|
|
789
|
+
const bit = (1 << at) | 0;
|
|
790
|
+
if ((lanes & bit) !== 0) {
|
|
791
|
+
decodeAt(at);
|
|
792
|
+
lanes = (lanes ^ bit) | 0;
|
|
793
|
+
done = (done ^ bit) | 0;
|
|
794
|
+
if (lanes === 0)
|
|
795
|
+
break;
|
|
796
|
+
}
|
|
797
|
+
at = at === regionLow ? regionHigh : at - 1;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
finally {
|
|
801
|
+
// Publish the ACK before clearing intent.
|
|
802
|
+
LastWorker = (LastWorker ^ done) | 0;
|
|
803
|
+
if (done !== 0)
|
|
804
|
+
Atomics.xor(workerBits, 0, done);
|
|
805
|
+
a_store(stealView, stealWantIndex[stealId], 0);
|
|
806
|
+
stealCursor = stealHome;
|
|
807
|
+
}
|
|
808
|
+
return true;
|
|
809
|
+
};
|
|
810
|
+
/** Claim one region in the shared owner mask. */
|
|
811
|
+
const decodeStealCasMask = () => {
|
|
812
|
+
const pending = (a_load(hostBits, 0) ^ a_load(workerBits, 0)) | 0;
|
|
813
|
+
if (pending === 0)
|
|
814
|
+
return false;
|
|
815
|
+
let owners = a_load(stealView, stealMaskIndex) | 0;
|
|
816
|
+
let region = -1;
|
|
817
|
+
let intent = 0 | 0;
|
|
818
|
+
for (let step = 0; step < stealRegions && region < 0; step++) {
|
|
819
|
+
const candidate = stealCursor + step < stealRegions
|
|
820
|
+
? stealCursor + step
|
|
821
|
+
: stealCursor + step - stealRegions;
|
|
822
|
+
if ((pending & stealLaneMask(candidate)) === 0)
|
|
823
|
+
continue;
|
|
824
|
+
const bit = (1 << candidate) | 0;
|
|
825
|
+
// Retry a changed mask once before trying another region.
|
|
826
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
827
|
+
if ((owners & bit) !== 0)
|
|
828
|
+
break;
|
|
829
|
+
const previous = a_compareExchange(stealView, stealMaskIndex, owners, (owners | bit) | 0) | 0;
|
|
830
|
+
if (previous === owners) {
|
|
831
|
+
region = candidate;
|
|
832
|
+
intent = bit;
|
|
833
|
+
break;
|
|
834
|
+
}
|
|
835
|
+
owners = previous;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
if (region < 0)
|
|
839
|
+
return false;
|
|
840
|
+
a_store(stealView, stealWantIndex[stealId], intent);
|
|
841
|
+
const take = ((a_load(hostBits, 0) ^ a_load(workerBits, 0)) &
|
|
842
|
+
stealLaneMask(region)) | 0;
|
|
843
|
+
if (take === 0) {
|
|
844
|
+
a_store(stealView, stealWantIndex[stealId], 0);
|
|
845
|
+
Atomics.and(stealView, stealMaskIndex, ~intent);
|
|
846
|
+
return false;
|
|
847
|
+
}
|
|
848
|
+
let lanes = take;
|
|
849
|
+
let done = 0 | 0;
|
|
850
|
+
// Decode the claimed lanes in producer order.
|
|
851
|
+
let at = stealOldestLane(region, take);
|
|
852
|
+
const regionLow = (region * stealRegionLanes) | 0;
|
|
853
|
+
const regionHigh = (regionLow + stealRegionLanes - 1) | 0;
|
|
854
|
+
try {
|
|
855
|
+
for (let step = 0; step < stealRegionLanes; step++) {
|
|
856
|
+
const bit = (1 << at) | 0;
|
|
857
|
+
if ((lanes & bit) !== 0) {
|
|
858
|
+
decodeAt(at);
|
|
859
|
+
lanes = (lanes ^ bit) | 0;
|
|
860
|
+
done = (done ^ bit) | 0;
|
|
861
|
+
if (lanes === 0)
|
|
862
|
+
break;
|
|
863
|
+
}
|
|
864
|
+
at = at === regionLow ? regionHigh : at - 1;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
finally {
|
|
868
|
+
LastWorker = (LastWorker ^ done) | 0;
|
|
869
|
+
if (done !== 0)
|
|
870
|
+
Atomics.xor(workerBits, 0, done);
|
|
871
|
+
a_store(stealView, stealWantIndex[stealId], 0);
|
|
872
|
+
Atomics.and(stealView, stealMaskIndex, ~intent);
|
|
873
|
+
stealCursor = stealHome;
|
|
874
|
+
}
|
|
768
875
|
return true;
|
|
769
876
|
};
|
|
770
|
-
/**
|
|
771
|
-
* HOST SIDE: decode version
|
|
772
|
-
*/
|
|
773
877
|
const resolveHost = ({ queue, onResolved, shouldSettle, activeRejectPlaceholder, }) => {
|
|
774
878
|
const getTask = takeTask({ queue });
|
|
775
879
|
let lastResolved = 32;
|
|
@@ -895,27 +999,21 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
895
999
|
return modified;
|
|
896
1000
|
};
|
|
897
1001
|
};
|
|
898
|
-
/**
|
|
899
|
-
* HOST SIDE: wait until the producer changes hostBits.
|
|
900
|
-
*
|
|
901
|
-
* LastWorker is the host's acknowledgement shadow. Passing it as the
|
|
902
|
-
* expected value makes the arm race-free: a publication between the drain
|
|
903
|
-
* and this call returns `not-equal` synchronously instead of being lost.
|
|
904
|
-
*/
|
|
1002
|
+
/** Wait for a producer publication without losing an intervening wakeup. */
|
|
905
1003
|
const waitForHostChange = (timeoutMs) => {
|
|
906
1004
|
if (a_waitAsync === undefined) {
|
|
907
|
-
a_store(doorbellArmed, 0,
|
|
1005
|
+
a_store(doorbellArmed, 0, DOORBELL_OFF);
|
|
908
1006
|
return undefined;
|
|
909
1007
|
}
|
|
910
|
-
a_store(doorbellArmed, 0,
|
|
1008
|
+
a_store(doorbellArmed, 0, DOORBELL_ARMED);
|
|
911
1009
|
try {
|
|
912
1010
|
const wait = a_waitAsync(hostBits, 0, LastWorker | 0, timeoutMs);
|
|
913
1011
|
if (!wait.async)
|
|
914
|
-
a_store(doorbellArmed, 0,
|
|
1012
|
+
a_store(doorbellArmed, 0, DOORBELL_OFF);
|
|
915
1013
|
return wait;
|
|
916
1014
|
}
|
|
917
1015
|
catch {
|
|
918
|
-
a_store(doorbellArmed, 0,
|
|
1016
|
+
a_store(doorbellArmed, 0, DOORBELL_OFF);
|
|
919
1017
|
// Some runtimes reject particular SharedArrayBuffer implementations
|
|
920
1018
|
// (for example a growable or native-backed buffer). Fall back to the
|
|
921
1019
|
// existing dispatcher rather than turning a capability issue into a
|
|
@@ -923,6 +1021,18 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
923
1021
|
return undefined;
|
|
924
1022
|
}
|
|
925
1023
|
};
|
|
1024
|
+
/**
|
|
1025
|
+
* Arm a native host notifier with the same no-lost-wakeup property as
|
|
1026
|
+
* waitAsync: a publication before or during the arm is observed directly,
|
|
1027
|
+
* while one after the observation sees the shared armed bit and rings.
|
|
1028
|
+
*/
|
|
1029
|
+
const armHostNotifier = () => {
|
|
1030
|
+
a_store(doorbellArmed, 0, DOORBELL_ARMED);
|
|
1031
|
+
if (a_load(hostBits, 0) === (LastWorker | 0))
|
|
1032
|
+
return true;
|
|
1033
|
+
a_store(doorbellArmed, 0, DOORBELL_OFF);
|
|
1034
|
+
return false;
|
|
1035
|
+
};
|
|
926
1036
|
const decodeAt = (at) => {
|
|
927
1037
|
const off = (at * headersSlotStride) + slotBaseU32;
|
|
928
1038
|
const recycled = recycleShift();
|
|
@@ -980,6 +1090,11 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
980
1090
|
}
|
|
981
1091
|
const bit = 1 << id;
|
|
982
1092
|
const previous = Atomics.and(stealView, stealLiveIndex, ~bit);
|
|
1093
|
+
if (stealClaimMask) {
|
|
1094
|
+
const stranded = a_load(stealView, stealWantIndex[id]) | 0;
|
|
1095
|
+
if (stranded !== 0)
|
|
1096
|
+
Atomics.and(stealView, stealMaskIndex, ~stranded);
|
|
1097
|
+
}
|
|
983
1098
|
return (previous & bit) !== 0;
|
|
984
1099
|
};
|
|
985
1100
|
return {
|
|
@@ -989,7 +1104,9 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
989
1104
|
encodeAll,
|
|
990
1105
|
publish,
|
|
991
1106
|
flushPending,
|
|
992
|
-
decode: stealEnabled
|
|
1107
|
+
decode: stealEnabled
|
|
1108
|
+
? (stealClaimMask ? decodeStealCasMask : decodeSteal)
|
|
1109
|
+
: decode,
|
|
993
1110
|
hasSpace,
|
|
994
1111
|
resolved,
|
|
995
1112
|
hostBits,
|
|
@@ -997,8 +1114,9 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
997
1114
|
recyclecList,
|
|
998
1115
|
resolveHost,
|
|
999
1116
|
waitForHostChange,
|
|
1117
|
+
armHostNotifier,
|
|
1000
1118
|
setHostWaiterArmed: (armed) => {
|
|
1001
|
-
a_store(doorbellArmed, 0, armed ?
|
|
1119
|
+
a_store(doorbellArmed, 0, armed ? DOORBELL_ARMED : DOORBELL_OFF);
|
|
1002
1120
|
},
|
|
1003
1121
|
hasPendingFrames: () => toBeSent.size !== 0,
|
|
1004
1122
|
getPendingFrameCount: () => toBeSent.size | 0,
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { type PromisePayloadHandler, type Task } from "./lock.js";
|
|
2
2
|
import type { LockBufferTextCompat } from "../common/shared-buffer-text.js";
|
|
3
3
|
import { type PayloadBufferOptions } from "./payload-config.js";
|
|
4
|
-
import type
|
|
4
|
+
import { type SharedBufferSource } from "../common/shared-buffer-region.js";
|
|
5
|
+
/** Minimum size for automatically borrowed byte returns. */
|
|
6
|
+
export declare const SHARED_RETURN_MIN_BYTES: number;
|
|
7
|
+
/** Number of later borrowed returns kept before a region is recycled. */
|
|
8
|
+
export declare const SHARED_RETURN_BORROW_WINDOW = 32;
|
|
9
|
+
/** The borrowed-region allocator for `payload`, if an encoder built one. */
|
|
10
|
+
export declare const getSharedReturnAllocator: (payload: object) => ((byteLength: number, zeroFill?: boolean) => Uint8Array | undefined) | undefined;
|
|
5
11
|
/**
|
|
6
12
|
* Returns `true` when the payload is encoded successfully.
|
|
7
13
|
* Returns `false` when dynamic payload space could not be reserved.
|
|
8
14
|
*/
|
|
9
|
-
export declare const encodePayload: ({ lockSector, payload, sab, payloadConfig, headersBuffer, headerSlotStrideU32, textCompat, onPromise, processBoundary, }: {
|
|
15
|
+
export declare const encodePayload: ({ lockSector, payload, sab, payloadConfig, headersBuffer, headerSlotStrideU32, textCompat, onPromise, processBoundary, sharedReturn, moveReturn, }: {
|
|
10
16
|
lockSector?: SharedBufferSource;
|
|
11
17
|
payload?: {
|
|
12
18
|
sab?: SharedBufferSource;
|
|
@@ -29,6 +35,16 @@ export declare const encodePayload: ({ lockSector, payload, sab, payloadConfig,
|
|
|
29
35
|
* Process transports must set this on both encoders and decoders.
|
|
30
36
|
*/
|
|
31
37
|
processBoundary?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Enable explicit `sharedBytes()` allocations on a worker return lane.
|
|
40
|
+
* Ordinary byte returns never become borrowed implicitly.
|
|
41
|
+
*/
|
|
42
|
+
sharedReturn?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Move large top-level byte returns into an owned host ArrayBuffer. Unlike a
|
|
45
|
+
* borrowed return, the result stays valid until the host drops it.
|
|
46
|
+
*/
|
|
47
|
+
moveReturn?: boolean;
|
|
32
48
|
}) => (task: Task, slotIndex: number) => boolean;
|
|
33
49
|
export declare const decodePayload: ({ lockSector, payload, sab, payloadConfig, headersBuffer, headerSlotStrideU32, textCompat, host, processBoundary, }: {
|
|
34
50
|
lockSector?: SharedBufferSource;
|