knitting 0.1.63 → 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 +105 -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/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 +205 -79
- 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 +21 -15
- package/src/runtime/pool.js +104 -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/types.d.ts +35 -71
- package/src/worker/loop.js +79 -57
- 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
|
/**
|
|
@@ -248,6 +257,13 @@ export const STEAL_PAYLOAD_ACK_SLOT_OFFSET_U32 = STEAL_ACK_SLOT_OFFSET_U32 + 2;
|
|
|
248
257
|
export const STEAL_LIVE_SLOT_OFFSET_U32 = STEAL_PAYLOAD_ACK_SLOT_OFFSET_U32 + 1;
|
|
249
258
|
/** Host-owned arm word for the return-lock completion doorbell. */
|
|
250
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;
|
|
251
267
|
export const HEADER_U32_LENGTH = LockBound.header +
|
|
252
268
|
(HEADER_SLOT_STRIDE_U32 * LockBound.slots);
|
|
253
269
|
export const HEADER_BYTE_LENGTH = HEADER_U32_LENGTH *
|
|
@@ -287,18 +303,17 @@ const makeTaskFrom = (array, at) => {
|
|
|
287
303
|
fillTaskFrom(task, array, at);
|
|
288
304
|
return task;
|
|
289
305
|
};
|
|
290
|
-
// could be inlined
|
|
291
306
|
const settleTask = (task) => {
|
|
292
307
|
if (task[TaskIndex["FlagsToHost"]] === 0) {
|
|
293
308
|
task.resolve(task.value);
|
|
294
309
|
}
|
|
295
310
|
else {
|
|
296
311
|
task.reject(task.value);
|
|
297
|
-
//
|
|
312
|
+
// Clear the rejection marker before the task slot is reused.
|
|
298
313
|
task[TaskIndex["FlagsToHost"]] = 0;
|
|
299
314
|
}
|
|
300
315
|
};
|
|
301
|
-
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, }) => {
|
|
302
317
|
// Layout within `lockSectorRegion`:
|
|
303
318
|
// - hostBits starts at byte 0
|
|
304
319
|
// - workerBits starts at byte 64
|
|
@@ -306,9 +321,8 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
306
321
|
// The remaining two cache lines in the 256-byte sector are reserved for the
|
|
307
322
|
// payload allocator lock (`PAYLOAD_LOCK_*` at bytes 128 and 192).
|
|
308
323
|
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
// 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.
|
|
312
326
|
const lockSectorRegion = toSharedBufferRegion(LockBoundSector ??
|
|
313
327
|
createWasmSharedArrayBuffer(LOCK_SECTOR_BYTE_LENGTH));
|
|
314
328
|
const LockBoundSAB = lockSectorRegion.sab;
|
|
@@ -335,23 +349,22 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
335
349
|
headers: headersRegion,
|
|
336
350
|
payload: payloadSAB,
|
|
337
351
|
});
|
|
338
|
-
//
|
|
339
|
-
// With N consumers the pending set is A ^ ACK[0] ^ ... ^ ACK[N-1]; every word
|
|
340
|
-
// 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].
|
|
341
353
|
const stealConsumers = Math.max(1, (consumers ?? 1) | 0);
|
|
342
354
|
const stealEnabled = stealConsumers > 1;
|
|
343
|
-
//
|
|
344
|
-
// never claims. It still frees payload slots, so it needs an acknowledgement
|
|
345
|
-
// word of its own rather than sharing consumer 0's.
|
|
355
|
+
// The producer encodes and frees payload slots but never claims a region.
|
|
346
356
|
const stealIsProducer = consumerId === undefined;
|
|
347
357
|
const stealId = (consumerId ?? 0) | 0;
|
|
348
358
|
const stealRegionLanes = (regionLanes ?? 8) | 0;
|
|
349
359
|
const stealRegions = (LockBound.slots / stealRegionLanes) | 0;
|
|
360
|
+
const stealClaimMask = stealClaim === "cas-mask";
|
|
350
361
|
if (stealEnabled) {
|
|
351
362
|
if (stealRegionLanes < 1 || (stealRegionLanes & (stealRegionLanes - 1)) !== 0) {
|
|
352
363
|
throw new RangeError("regionLanes must be a power of two");
|
|
353
364
|
}
|
|
354
|
-
|
|
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) {
|
|
355
368
|
throw new RangeError(`regionLanes=${stealRegionLanes} yields ${stealRegions} regions, ` +
|
|
356
369
|
`too few for ${stealConsumers} consumers`);
|
|
357
370
|
}
|
|
@@ -368,11 +381,16 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
368
381
|
STEAL_WANT_SLOT_OFFSET_U32;
|
|
369
382
|
}
|
|
370
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;
|
|
371
386
|
const stealAllLiveMask = stealConsumers === 32
|
|
372
387
|
? -1
|
|
373
388
|
: ((1 << stealConsumers) - 1) | 0;
|
|
374
389
|
if (stealEnabled && stealIsProducer) {
|
|
375
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);
|
|
376
394
|
}
|
|
377
395
|
const stealIsLive = (mask, consumer) => (mask & (1 << consumer)) !== 0;
|
|
378
396
|
let promiseHandler;
|
|
@@ -391,6 +409,8 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
391
409
|
lockSector: payloadLockRegion,
|
|
392
410
|
textCompat: resolvedTextCompat,
|
|
393
411
|
processBoundary,
|
|
412
|
+
sharedReturn,
|
|
413
|
+
moveReturn,
|
|
394
414
|
onPromise: (task, isRejected, value) => {
|
|
395
415
|
if ((task[TASK_LOCAL_FLAGS_INDEX] & TASK_LOCAL_PROMISE_TRACKED_FLAG) !==
|
|
396
416
|
0 &&
|
|
@@ -422,9 +442,9 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
422
442
|
const resolved = resultList ?? new RingQueue();
|
|
423
443
|
let deferredCount = 0 | 0;
|
|
424
444
|
let pendingPromiseCount = 0 | 0;
|
|
425
|
-
// Atomics aliases (hot path)
|
|
426
445
|
const a_load = Atomics.load;
|
|
427
446
|
const a_store = Atomics.store;
|
|
447
|
+
const a_compareExchange = Atomics.compareExchange;
|
|
428
448
|
const a_notify = Atomics.notify;
|
|
429
449
|
const shouldNotifyHostPublish = notifyOnHostPublish === true;
|
|
430
450
|
const a_waitAsync = typeof Atomics.waitAsync === "function"
|
|
@@ -432,14 +452,13 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
432
452
|
: undefined;
|
|
433
453
|
// Sender-side cached shadow of the receiver-owned queue word. Under the XSC
|
|
434
454
|
// false-busy-only sender-side staleness property, this may hide newly freed
|
|
435
|
-
// lanes but cannot make a
|
|
455
|
+
// lanes but cannot make a pending lane appear free. Refresh only
|
|
436
456
|
// when the cached free set is exhausted.
|
|
437
457
|
let workerShadow = 0 | 0;
|
|
438
458
|
// Under stealing, workerBits also records retired lanes.
|
|
439
459
|
const refreshWorkerShadow = () => workerShadow = a_load(workerBits, 0) | 0;
|
|
440
460
|
refreshWorkerShadow();
|
|
441
461
|
const ensureSenderStateHasFree = (state) => (~state) !== 0 ? state : (LastLocal ^ refreshWorkerShadow()) | 0;
|
|
442
|
-
// RingQueue method aliases (hot path)
|
|
443
462
|
const toBeSentPush = (task) => toBeSent.push(task);
|
|
444
463
|
const toBeSentShift = () => toBeSent.shiftNoClear();
|
|
445
464
|
const toBeSentUnshift = (task) => toBeSent.unshift(task);
|
|
@@ -563,8 +582,22 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
563
582
|
};
|
|
564
583
|
const storeHost = (bit) => {
|
|
565
584
|
a_store(hostBits, 0, LastLocal = (LastLocal ^ bit) | 0);
|
|
566
|
-
if (shouldNotifyHostPublish &&
|
|
567
|
-
|
|
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
|
+
}
|
|
568
601
|
}
|
|
569
602
|
};
|
|
570
603
|
const storeWorker = (bit) => a_store(workerBits, 0, LastWorker = (LastWorker ^ bit) | 0);
|
|
@@ -607,9 +640,6 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
607
640
|
return true;
|
|
608
641
|
};
|
|
609
642
|
const hasSpace = () => (hostBits[0] ^ LastWorker) !== 0;
|
|
610
|
-
/**
|
|
611
|
-
* WORKER SIDE: decode
|
|
612
|
-
*/
|
|
613
643
|
const decode = () => {
|
|
614
644
|
let diff = (a_load(hostBits, 0) ^ LastWorker) | 0;
|
|
615
645
|
if (diff === 0)
|
|
@@ -640,21 +670,21 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
640
670
|
lastTake = last;
|
|
641
671
|
return true;
|
|
642
672
|
};
|
|
643
|
-
/**
|
|
644
|
-
* WORKER SIDE: decode, region-Dekker stealing variant.
|
|
645
|
-
*
|
|
646
|
-
* Claims one whole region of `stealRegionLanes` lanes per handshake, so the
|
|
647
|
-
* StoreLoad barrier is amortised over the region rather than paid per task.
|
|
648
|
-
* Priority is fixed by endpoint id: a junior withdraws for a senior, so at
|
|
649
|
-
* most one consumer ever acknowledges a published generation.
|
|
650
|
-
*
|
|
651
|
-
* The C reference needs an explicit mfence between the intent store and the
|
|
652
|
-
* peer loads. `Atomics.store` is sequentially consistent in JS, so that
|
|
653
|
-
* barrier is implicit here and holds on x86 and ARM alike.
|
|
654
|
-
*/
|
|
655
673
|
const stealLaneMask = (region) => stealRegionLanes === 32
|
|
656
674
|
? -1
|
|
657
675
|
: ((((1 << stealRegionLanes) - 1) << (region * stealRegionLanes)) | 0);
|
|
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));
|
|
687
|
+
};
|
|
658
688
|
const stealJuniorWants = (intent) => {
|
|
659
689
|
const live = a_load(stealView, stealLiveIndex) | 0;
|
|
660
690
|
for (let c = stealId + 1; c < stealConsumers; c++) {
|
|
@@ -667,6 +697,7 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
667
697
|
};
|
|
668
698
|
const stealHome = ((stealRegions * stealId) / stealConsumers) | 0;
|
|
669
699
|
let stealCursor = stealHome;
|
|
700
|
+
/** Decode one region using the Dekker claim protocol. */
|
|
670
701
|
const decodeSteal = () => {
|
|
671
702
|
// workerBits includes retired lanes under stealing.
|
|
672
703
|
const pending = (a_load(hostBits, 0) ^ a_load(workerBits, 0)) | 0;
|
|
@@ -682,6 +713,8 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
682
713
|
}
|
|
683
714
|
}
|
|
684
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.
|
|
685
718
|
let peerIntent = 0 | 0;
|
|
686
719
|
let seniorIntent = 0 | 0;
|
|
687
720
|
for (let c = 0; c < stealConsumers; c++) {
|
|
@@ -709,59 +742,138 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
709
742
|
return false;
|
|
710
743
|
const intent = (1 << region) | 0;
|
|
711
744
|
a_store(stealView, stealWantIndex[stealId], intent);
|
|
712
|
-
//
|
|
745
|
+
// Atomics.store is sequentially consistent, so it supplies the StoreLoad
|
|
746
|
+
// barrier required by Dekker.
|
|
747
|
+
// A senior claimant has priority.
|
|
713
748
|
let seniorConflict = false;
|
|
714
|
-
let
|
|
715
|
-
|
|
716
|
-
if (c === stealId || !stealIsLive(liveBeforeClaim, c))
|
|
749
|
+
for (let c = 0; c < stealId; c++) {
|
|
750
|
+
if (!stealIsLive(liveBeforeClaim, c))
|
|
717
751
|
continue;
|
|
718
|
-
if ((a_load(stealView, stealWantIndex[c]) & intent)
|
|
719
|
-
continue;
|
|
720
|
-
if (c < stealId)
|
|
752
|
+
if ((a_load(stealView, stealWantIndex[c]) & intent) !== 0) {
|
|
721
753
|
seniorConflict = true;
|
|
722
|
-
|
|
723
|
-
|
|
754
|
+
break;
|
|
755
|
+
}
|
|
724
756
|
}
|
|
725
757
|
if (seniorConflict) {
|
|
726
758
|
a_store(stealView, stealWantIndex[stealId], 0);
|
|
727
759
|
return false;
|
|
728
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
|
+
}
|
|
729
770
|
if (juniorConflict) {
|
|
730
771
|
while (stealJuniorWants(intent)) { /* junior withdraws */ }
|
|
731
772
|
}
|
|
732
|
-
//
|
|
733
|
-
// until every slot in `take` has been decoded.
|
|
773
|
+
// No control word is read again until all selected slots are decoded.
|
|
734
774
|
const take = ((a_load(hostBits, 0) ^ a_load(workerBits, 0)) &
|
|
735
775
|
stealLaneMask(region)) | 0;
|
|
736
776
|
if (take === 0) {
|
|
737
777
|
a_store(stealView, stealWantIndex[stealId], 0);
|
|
738
778
|
return false;
|
|
739
779
|
}
|
|
740
|
-
// Retire
|
|
780
|
+
// Retire lanes and release the claim even if decoding throws.
|
|
741
781
|
let lanes = take;
|
|
742
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;
|
|
743
787
|
try {
|
|
744
|
-
|
|
745
|
-
const bit = (
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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;
|
|
749
798
|
}
|
|
750
799
|
}
|
|
751
800
|
finally {
|
|
752
|
-
// ACK before clearing intent.
|
|
801
|
+
// Publish the ACK before clearing intent.
|
|
753
802
|
LastWorker = (LastWorker ^ done) | 0;
|
|
754
803
|
if (done !== 0)
|
|
755
804
|
Atomics.xor(workerBits, 0, done);
|
|
756
805
|
a_store(stealView, stealWantIndex[stealId], 0);
|
|
757
|
-
// Restart from this consumer's home region.
|
|
758
806
|
stealCursor = stealHome;
|
|
759
807
|
}
|
|
760
808
|
return true;
|
|
761
809
|
};
|
|
762
|
-
/**
|
|
763
|
-
|
|
764
|
-
|
|
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
|
+
}
|
|
875
|
+
return true;
|
|
876
|
+
};
|
|
765
877
|
const resolveHost = ({ queue, onResolved, shouldSettle, activeRejectPlaceholder, }) => {
|
|
766
878
|
const getTask = takeTask({ queue });
|
|
767
879
|
let lastResolved = 32;
|
|
@@ -887,27 +999,21 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
887
999
|
return modified;
|
|
888
1000
|
};
|
|
889
1001
|
};
|
|
890
|
-
/**
|
|
891
|
-
* HOST SIDE: wait until the producer changes hostBits.
|
|
892
|
-
*
|
|
893
|
-
* LastWorker is the host's acknowledgement shadow. Passing it as the
|
|
894
|
-
* expected value makes the arm race-free: a publication between the drain
|
|
895
|
-
* and this call returns `not-equal` synchronously instead of being lost.
|
|
896
|
-
*/
|
|
1002
|
+
/** Wait for a producer publication without losing an intervening wakeup. */
|
|
897
1003
|
const waitForHostChange = (timeoutMs) => {
|
|
898
1004
|
if (a_waitAsync === undefined) {
|
|
899
|
-
a_store(doorbellArmed, 0,
|
|
1005
|
+
a_store(doorbellArmed, 0, DOORBELL_OFF);
|
|
900
1006
|
return undefined;
|
|
901
1007
|
}
|
|
902
|
-
a_store(doorbellArmed, 0,
|
|
1008
|
+
a_store(doorbellArmed, 0, DOORBELL_ARMED);
|
|
903
1009
|
try {
|
|
904
1010
|
const wait = a_waitAsync(hostBits, 0, LastWorker | 0, timeoutMs);
|
|
905
1011
|
if (!wait.async)
|
|
906
|
-
a_store(doorbellArmed, 0,
|
|
1012
|
+
a_store(doorbellArmed, 0, DOORBELL_OFF);
|
|
907
1013
|
return wait;
|
|
908
1014
|
}
|
|
909
1015
|
catch {
|
|
910
|
-
a_store(doorbellArmed, 0,
|
|
1016
|
+
a_store(doorbellArmed, 0, DOORBELL_OFF);
|
|
911
1017
|
// Some runtimes reject particular SharedArrayBuffer implementations
|
|
912
1018
|
// (for example a growable or native-backed buffer). Fall back to the
|
|
913
1019
|
// existing dispatcher rather than turning a capability issue into a
|
|
@@ -915,6 +1021,18 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
915
1021
|
return undefined;
|
|
916
1022
|
}
|
|
917
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
|
+
};
|
|
918
1036
|
const decodeAt = (at) => {
|
|
919
1037
|
const off = (at * headersSlotStride) + slotBaseU32;
|
|
920
1038
|
const recycled = recycleShift();
|
|
@@ -972,6 +1090,11 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
972
1090
|
}
|
|
973
1091
|
const bit = 1 << id;
|
|
974
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
|
+
}
|
|
975
1098
|
return (previous & bit) !== 0;
|
|
976
1099
|
};
|
|
977
1100
|
return {
|
|
@@ -981,7 +1104,9 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
981
1104
|
encodeAll,
|
|
982
1105
|
publish,
|
|
983
1106
|
flushPending,
|
|
984
|
-
decode: stealEnabled
|
|
1107
|
+
decode: stealEnabled
|
|
1108
|
+
? (stealClaimMask ? decodeStealCasMask : decodeSteal)
|
|
1109
|
+
: decode,
|
|
985
1110
|
hasSpace,
|
|
986
1111
|
resolved,
|
|
987
1112
|
hostBits,
|
|
@@ -989,8 +1114,9 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
989
1114
|
recyclecList,
|
|
990
1115
|
resolveHost,
|
|
991
1116
|
waitForHostChange,
|
|
1117
|
+
armHostNotifier,
|
|
992
1118
|
setHostWaiterArmed: (armed) => {
|
|
993
|
-
a_store(doorbellArmed, 0, armed ?
|
|
1119
|
+
a_store(doorbellArmed, 0, armed ? DOORBELL_ARMED : DOORBELL_OFF);
|
|
994
1120
|
},
|
|
995
1121
|
hasPendingFrames: () => toBeSent.size !== 0,
|
|
996
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;
|