knitting 0.1.60 → 0.1.62
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 +94 -6
- package/knitting.browser.d.ts +6 -0
- package/knitting.browser.js +1 -0
- package/package.json +11 -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/src/api.js +136 -25
- package/src/common/runtime.d.ts +2 -1
- package/src/common/runtime.js +8 -1
- package/src/common/task-source.d.ts +9 -0
- package/src/common/task-source.js +15 -0
- package/src/memory/lock.d.ts +37 -1
- package/src/memory/lock.js +262 -5
- package/src/memory/payloadCodec.js +2 -6
- package/src/memory/regionRegistry.d.ts +1 -3
- package/src/memory/regionRegistry.js +16 -14
- package/src/permission/protocol.js +5 -1
- package/src/runtime/dispatcher.d.ts +11 -8
- package/src/runtime/dispatcher.js +163 -21
- package/src/runtime/inline-executor.js +6 -1
- package/src/runtime/pool.d.ts +127 -4
- package/src/runtime/pool.js +262 -73
- package/src/runtime/process-worker.d.ts +24 -12
- package/src/runtime/process-worker.js +185 -115
- package/src/runtime/tx-queue.d.ts +17 -1
- package/src/runtime/tx-queue.js +120 -15
- package/src/runtime/worker-common.d.ts +13 -0
- package/src/runtime/worker-common.js +103 -0
- package/src/types.d.ts +75 -4
- package/src/worker/loop.js +34 -5
- package/src/worker/rx-queue.d.ts +11 -1
- package/src/worker/rx-queue.js +4 -1
- package/src/worker/timers.d.ts +8 -1
- package/src/worker/timers.js +20 -17
package/src/memory/lock.js
CHANGED
|
@@ -226,6 +226,29 @@ export const HEADER_TASK_LINE_U32 = LOCK_CACHE_LINE_BYTES /
|
|
|
226
226
|
export const HEADER_STATIC_PAYLOAD_U32 = TaskIndex.TotalBuff -
|
|
227
227
|
HEADER_TASK_LINE_U32;
|
|
228
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;
|
|
250
|
+
/** Host-owned arm word for the return-lock completion doorbell. */
|
|
251
|
+
export const DOORBELL_ARMED_SLOT_OFFSET_U32 = STEAL_LIVE_SLOT_OFFSET_U32 + 1;
|
|
229
252
|
export const HEADER_U32_LENGTH = LockBound.header +
|
|
230
253
|
(HEADER_SLOT_STRIDE_U32 * LockBound.slots);
|
|
231
254
|
export const HEADER_BYTE_LENGTH = HEADER_U32_LENGTH *
|
|
@@ -276,7 +299,7 @@ const settleTask = (task) => {
|
|
|
276
299
|
task[TaskIndex["FlagsToHost"]] = 0;
|
|
277
300
|
}
|
|
278
301
|
};
|
|
279
|
-
export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload, payloadConfig, payloadSector, textCompat, resultList, toSentList, recycleList, processBoundary, }) => {
|
|
302
|
+
export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload, payloadConfig, payloadSector, textCompat, resultList, toSentList, recycleList, processBoundary, consumers, consumerId, regionLanes, notifyOnHostPublish, }) => {
|
|
280
303
|
// Layout within `lockSectorRegion`:
|
|
281
304
|
// - hostBits starts at byte 0
|
|
282
305
|
// - workerBits starts at byte 64
|
|
@@ -295,6 +318,11 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
295
318
|
const headersRegion = toSharedBufferRegion(headers ?? createWasmSharedArrayBuffer(HEADER_BYTE_LENGTH));
|
|
296
319
|
const headersBuffer = new Uint32Array(headersRegion.sab, headersRegion.byteOffset, headersRegion.byteLength >>> 2);
|
|
297
320
|
const headersSlotStride = headerSlotStrideU32 ?? HEADER_SLOT_STRIDE_U32;
|
|
321
|
+
// The first task cache line has four unused control words after the task
|
|
322
|
+
// header. Keep the doorbell arm bit in one of those words so the host and
|
|
323
|
+
// worker lock instances can share it without allocating another SAB.
|
|
324
|
+
const doorbellArmed = new Int32Array(headersRegion.sab, headersRegion.byteOffset +
|
|
325
|
+
(DOORBELL_ARMED_SLOT_OFFSET_U32 * Uint32Array.BYTES_PER_ELEMENT), 1);
|
|
298
326
|
const resolvedPayloadConfig = resolvePayloadBufferOptions({
|
|
299
327
|
sab: payload,
|
|
300
328
|
options: payloadConfig,
|
|
@@ -308,6 +336,55 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
308
336
|
headers: headersRegion,
|
|
309
337
|
payload: payloadSAB,
|
|
310
338
|
});
|
|
339
|
+
// ---- work stealing (multi-consumer) state ----
|
|
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.
|
|
342
|
+
const stealConsumers = Math.max(1, (consumers ?? 1) | 0);
|
|
343
|
+
const stealEnabled = stealConsumers > 1;
|
|
344
|
+
// A lock2 built without `consumerId` is the producer endpoint: it encodes but
|
|
345
|
+
// never claims. It still frees payload slots, so it needs an acknowledgement
|
|
346
|
+
// word of its own rather than sharing consumer 0's.
|
|
347
|
+
const stealIsProducer = consumerId === undefined;
|
|
348
|
+
const stealId = (consumerId ?? 0) | 0;
|
|
349
|
+
const stealRegionLanes = (regionLanes ?? 8) | 0;
|
|
350
|
+
const stealRegions = (LockBound.slots / stealRegionLanes) | 0;
|
|
351
|
+
if (stealEnabled) {
|
|
352
|
+
if (stealRegionLanes < 1 || (stealRegionLanes & (stealRegionLanes - 1)) !== 0) {
|
|
353
|
+
throw new RangeError("regionLanes must be a power of two");
|
|
354
|
+
}
|
|
355
|
+
if (stealRegions < stealConsumers) {
|
|
356
|
+
throw new RangeError(`regionLanes=${stealRegionLanes} yields ${stealRegions} regions, ` +
|
|
357
|
+
`too few for ${stealConsumers} consumers`);
|
|
358
|
+
}
|
|
359
|
+
if (stealId < 0 || stealId >= stealConsumers) {
|
|
360
|
+
throw new RangeError(`consumerId ${stealId} out of range`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// Int32 alias over the headers SAB so the control words get signed bit math
|
|
364
|
+
// consistent with the rest of the protocol.
|
|
365
|
+
const stealView = new Int32Array(headersRegion.sab, headersRegion.byteOffset, headersRegion.byteLength >>> 2);
|
|
366
|
+
const stealAckIndex = new Int32Array(stealConsumers);
|
|
367
|
+
const stealWantIndex = new Int32Array(stealConsumers);
|
|
368
|
+
for (let c = 0; c < stealConsumers; c++) {
|
|
369
|
+
const slotBase = (c * headersSlotStride) + LockBound.header;
|
|
370
|
+
stealAckIndex[c] = slotBase + STEAL_ACK_SLOT_OFFSET_U32;
|
|
371
|
+
stealWantIndex[c] = slotBase + STEAL_WANT_SLOT_OFFSET_U32;
|
|
372
|
+
}
|
|
373
|
+
const stealLiveIndex = LockBound.header + STEAL_LIVE_SLOT_OFFSET_U32;
|
|
374
|
+
const stealAllLiveMask = stealConsumers === 32
|
|
375
|
+
? -1
|
|
376
|
+
: ((1 << stealConsumers) - 1) | 0;
|
|
377
|
+
if (stealEnabled && stealIsProducer) {
|
|
378
|
+
Atomics.store(stealView, stealLiveIndex, stealAllLiveMask);
|
|
379
|
+
}
|
|
380
|
+
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
|
+
};
|
|
311
388
|
let promiseHandler;
|
|
312
389
|
if (registeredEncodePayload === undefined ||
|
|
313
390
|
registeredDecodePayload === undefined) {
|
|
@@ -358,12 +435,20 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
358
435
|
// Atomics aliases (hot path)
|
|
359
436
|
const a_load = Atomics.load;
|
|
360
437
|
const a_store = Atomics.store;
|
|
438
|
+
const a_notify = Atomics.notify;
|
|
439
|
+
const shouldNotifyHostPublish = notifyOnHostPublish === true;
|
|
440
|
+
const a_waitAsync = typeof Atomics.waitAsync === "function"
|
|
441
|
+
? Atomics.waitAsync.bind(Atomics)
|
|
442
|
+
: undefined;
|
|
361
443
|
// Sender-side cached shadow of the receiver-owned queue word. Under the XSC
|
|
362
444
|
// false-busy-only sender-side staleness property, this may hide newly freed
|
|
363
445
|
// lanes but cannot make a genuinely pending lane appear free. Refresh only
|
|
364
446
|
// when the cached free set is exhausted.
|
|
365
|
-
let workerShadow =
|
|
366
|
-
const refreshWorkerShadow =
|
|
447
|
+
let workerShadow = 0 | 0;
|
|
448
|
+
const refreshWorkerShadow = stealEnabled
|
|
449
|
+
? () => workerShadow = stealAckXorAll()
|
|
450
|
+
: () => workerShadow = a_load(workerBits, 0) | 0;
|
|
451
|
+
refreshWorkerShadow();
|
|
367
452
|
const ensureSenderStateHasFree = (state) => (~state) !== 0 ? state : (LastLocal ^ refreshWorkerShadow()) | 0;
|
|
368
453
|
// RingQueue method aliases (hot path)
|
|
369
454
|
const toBeSentPush = (task) => toBeSent.push(task);
|
|
@@ -487,7 +572,12 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
487
572
|
deferredCount = 0 | 0;
|
|
488
573
|
return toBeSent.isEmpty;
|
|
489
574
|
};
|
|
490
|
-
const storeHost = (bit) =>
|
|
575
|
+
const storeHost = (bit) => {
|
|
576
|
+
a_store(hostBits, 0, LastLocal = (LastLocal ^ bit) | 0);
|
|
577
|
+
if (shouldNotifyHostPublish && a_load(doorbellArmed, 0) !== 0) {
|
|
578
|
+
a_notify(hostBits, 0, 1);
|
|
579
|
+
}
|
|
580
|
+
};
|
|
491
581
|
const storeWorker = (bit) => a_store(workerBits, 0, LastWorker = (LastWorker ^ bit) | 0);
|
|
492
582
|
const encode = (task, state = (LastLocal ^ workerShadow) | 0) => {
|
|
493
583
|
state = ensureSenderStateHasFree(state);
|
|
@@ -561,6 +651,122 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
561
651
|
lastTake = last;
|
|
562
652
|
return true;
|
|
563
653
|
};
|
|
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
|
+
const stealLaneMask = (region) => stealRegionLanes === 32
|
|
667
|
+
? -1
|
|
668
|
+
: ((((1 << stealRegionLanes) - 1) << (region * stealRegionLanes)) | 0);
|
|
669
|
+
const stealPeerAcks = () => {
|
|
670
|
+
let x = 0 | 0;
|
|
671
|
+
for (let c = 0; c < stealConsumers; c++) {
|
|
672
|
+
if (c !== stealId)
|
|
673
|
+
x = (x ^ a_load(stealView, stealAckIndex[c])) | 0;
|
|
674
|
+
}
|
|
675
|
+
return x;
|
|
676
|
+
};
|
|
677
|
+
const stealJuniorWants = (intent) => {
|
|
678
|
+
const live = a_load(stealView, stealLiveIndex) | 0;
|
|
679
|
+
for (let c = stealId + 1; c < stealConsumers; c++) {
|
|
680
|
+
if (!stealIsLive(live, c))
|
|
681
|
+
continue;
|
|
682
|
+
if ((a_load(stealView, stealWantIndex[c]) & intent) !== 0)
|
|
683
|
+
return true;
|
|
684
|
+
}
|
|
685
|
+
return false;
|
|
686
|
+
};
|
|
687
|
+
let stealCursor = ((stealRegions * stealId) / stealConsumers) | 0;
|
|
688
|
+
const decodeSteal = () => {
|
|
689
|
+
const pending = (a_load(hostBits, 0) ^ LastWorker ^ stealPeerAcks()) | 0;
|
|
690
|
+
if (pending === 0)
|
|
691
|
+
return false;
|
|
692
|
+
let pendingRegions = 0 | 0;
|
|
693
|
+
if (stealRegions === 1)
|
|
694
|
+
pendingRegions = 1;
|
|
695
|
+
else {
|
|
696
|
+
for (let r = 0; r < stealRegions; r++) {
|
|
697
|
+
if ((pending & stealLaneMask(r)) !== 0)
|
|
698
|
+
pendingRegions |= 1 << r;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
const liveBeforeClaim = a_load(stealView, stealLiveIndex) | 0;
|
|
702
|
+
let peerIntent = 0 | 0;
|
|
703
|
+
let seniorIntent = 0 | 0;
|
|
704
|
+
for (let c = 0; c < stealConsumers; c++) {
|
|
705
|
+
if (c === stealId || !stealIsLive(liveBeforeClaim, c))
|
|
706
|
+
continue;
|
|
707
|
+
const value = a_load(stealView, stealWantIndex[c]) | 0;
|
|
708
|
+
peerIntent |= value;
|
|
709
|
+
if (c < stealId)
|
|
710
|
+
seniorIntent |= value;
|
|
711
|
+
}
|
|
712
|
+
const notSenior = pendingRegions & ~seniorIntent;
|
|
713
|
+
if (notSenior === 0)
|
|
714
|
+
return false;
|
|
715
|
+
const clean = pendingRegions & ~peerIntent;
|
|
716
|
+
const candidates = clean !== 0 ? clean : notSenior;
|
|
717
|
+
let region = -1;
|
|
718
|
+
for (let step = 0; step < stealRegions; step++) {
|
|
719
|
+
const candidate = (stealCursor + step) % stealRegions;
|
|
720
|
+
if ((candidates & (1 << candidate)) !== 0) {
|
|
721
|
+
region = candidate;
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
if (region < 0)
|
|
726
|
+
return false;
|
|
727
|
+
const intent = (1 << region) | 0;
|
|
728
|
+
a_store(stealView, stealWantIndex[stealId], intent);
|
|
729
|
+
// Dekker StoreLoad: implicit, `Atomics.store` above is seq-cst.
|
|
730
|
+
let seniorConflict = false;
|
|
731
|
+
let juniorConflict = false;
|
|
732
|
+
for (let c = 0; c < stealConsumers; c++) {
|
|
733
|
+
if (c === stealId || !stealIsLive(liveBeforeClaim, c))
|
|
734
|
+
continue;
|
|
735
|
+
if ((a_load(stealView, stealWantIndex[c]) & intent) === 0)
|
|
736
|
+
continue;
|
|
737
|
+
if (c < stealId)
|
|
738
|
+
seniorConflict = true;
|
|
739
|
+
else
|
|
740
|
+
juniorConflict = true;
|
|
741
|
+
}
|
|
742
|
+
if (seniorConflict) {
|
|
743
|
+
a_store(stealView, stealWantIndex[stealId], 0);
|
|
744
|
+
return false;
|
|
745
|
+
}
|
|
746
|
+
if (juniorConflict) {
|
|
747
|
+
while (stealJuniorWants(intent)) { /* junior withdraws */ }
|
|
748
|
+
}
|
|
749
|
+
// Last control loads before ownership. After this no control word is read
|
|
750
|
+
// until every slot in `take` has been decoded.
|
|
751
|
+
const take = ((a_load(hostBits, 0) ^ LastWorker ^ stealPeerAcks()) &
|
|
752
|
+
stealLaneMask(region)) | 0;
|
|
753
|
+
if (take === 0) {
|
|
754
|
+
a_store(stealView, stealWantIndex[stealId], 0);
|
|
755
|
+
return false;
|
|
756
|
+
}
|
|
757
|
+
let lanes = take;
|
|
758
|
+
while (lanes !== 0) {
|
|
759
|
+
decodeAt(31 - clz32((lanes & -lanes) >>> 0));
|
|
760
|
+
lanes = (lanes & (lanes - 1)) | 0;
|
|
761
|
+
}
|
|
762
|
+
// Retire the whole claimed mask with one ACK store, then release the region.
|
|
763
|
+
// ACK before intent-clear (paper Assumption 2(ii)).
|
|
764
|
+
LastWorker = (LastWorker ^ take) | 0;
|
|
765
|
+
a_store(stealView, stealAckIndex[stealId], LastWorker);
|
|
766
|
+
a_store(stealView, stealWantIndex[stealId], 0);
|
|
767
|
+
stealCursor = (region + 1) % stealRegions;
|
|
768
|
+
return true;
|
|
769
|
+
};
|
|
564
770
|
/**
|
|
565
771
|
* HOST SIDE: decode version
|
|
566
772
|
*/
|
|
@@ -689,6 +895,34 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
689
895
|
return modified;
|
|
690
896
|
};
|
|
691
897
|
};
|
|
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
|
+
*/
|
|
905
|
+
const waitForHostChange = (timeoutMs) => {
|
|
906
|
+
if (a_waitAsync === undefined) {
|
|
907
|
+
a_store(doorbellArmed, 0, 0);
|
|
908
|
+
return undefined;
|
|
909
|
+
}
|
|
910
|
+
a_store(doorbellArmed, 0, 1);
|
|
911
|
+
try {
|
|
912
|
+
const wait = a_waitAsync(hostBits, 0, LastWorker | 0, timeoutMs);
|
|
913
|
+
if (!wait.async)
|
|
914
|
+
a_store(doorbellArmed, 0, 0);
|
|
915
|
+
return wait;
|
|
916
|
+
}
|
|
917
|
+
catch {
|
|
918
|
+
a_store(doorbellArmed, 0, 0);
|
|
919
|
+
// Some runtimes reject particular SharedArrayBuffer implementations
|
|
920
|
+
// (for example a growable or native-backed buffer). Fall back to the
|
|
921
|
+
// existing dispatcher rather than turning a capability issue into a
|
|
922
|
+
// hung pool.
|
|
923
|
+
return undefined;
|
|
924
|
+
}
|
|
925
|
+
};
|
|
692
926
|
const decodeAt = (at) => {
|
|
693
927
|
const off = (at * headersSlotStride) + slotBaseU32;
|
|
694
928
|
const recycled = recycleShift();
|
|
@@ -730,6 +964,24 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
730
964
|
deferredCount = 0 | 0;
|
|
731
965
|
pendingPromiseCount = 0 | 0;
|
|
732
966
|
};
|
|
967
|
+
/**
|
|
968
|
+
* HOST SIDE: remove a terminated consumer from stealing arbitration.
|
|
969
|
+
*
|
|
970
|
+
* Its ACK remains part of the generation XOR forever, because it records
|
|
971
|
+
* lanes that consumer retired while alive. Only its stale WANT becomes
|
|
972
|
+
* ineligible. The caller must invoke this only after the endpoint can no
|
|
973
|
+
* longer execute; there is deliberately no matching reactivation operation.
|
|
974
|
+
*/
|
|
975
|
+
const deactivateStealConsumer = (id) => {
|
|
976
|
+
if (!stealEnabled || !stealIsProducer)
|
|
977
|
+
return false;
|
|
978
|
+
if (!Number.isInteger(id) || id < 0 || id >= stealConsumers) {
|
|
979
|
+
throw new RangeError(`consumerId ${id} out of range`);
|
|
980
|
+
}
|
|
981
|
+
const bit = 1 << id;
|
|
982
|
+
const previous = Atomics.and(stealView, stealLiveIndex, ~bit);
|
|
983
|
+
return (previous & bit) !== 0;
|
|
984
|
+
};
|
|
733
985
|
return {
|
|
734
986
|
enlist,
|
|
735
987
|
encode,
|
|
@@ -737,17 +989,22 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
737
989
|
encodeAll,
|
|
738
990
|
publish,
|
|
739
991
|
flushPending,
|
|
740
|
-
decode,
|
|
992
|
+
decode: stealEnabled ? decodeSteal : decode,
|
|
741
993
|
hasSpace,
|
|
742
994
|
resolved,
|
|
743
995
|
hostBits,
|
|
744
996
|
workerBits,
|
|
745
997
|
recyclecList,
|
|
746
998
|
resolveHost,
|
|
999
|
+
waitForHostChange,
|
|
1000
|
+
setHostWaiterArmed: (armed) => {
|
|
1001
|
+
a_store(doorbellArmed, 0, armed ? 1 : 0);
|
|
1002
|
+
},
|
|
747
1003
|
hasPendingFrames: () => toBeSent.size !== 0,
|
|
748
1004
|
getPendingFrameCount: () => toBeSent.size | 0,
|
|
749
1005
|
getPendingPromiseCount: () => pendingPromiseCount | 0,
|
|
750
1006
|
resetPendingState,
|
|
1007
|
+
deactivateStealConsumer,
|
|
751
1008
|
takeDeferredCount: () => {
|
|
752
1009
|
const count = deferredCount | 0;
|
|
753
1010
|
deferredCount = 0 | 0;
|
|
@@ -311,9 +311,7 @@ export const encodePayload = ({ lockSector, payload, sab, payloadConfig, headers
|
|
|
311
311
|
options: payload?.config ?? payloadConfig,
|
|
312
312
|
});
|
|
313
313
|
const maxPayloadBytes = resolvedPayloadConfig.maxPayloadBytes;
|
|
314
|
-
const { allocTask, setSlotLength, free } = register({
|
|
315
|
-
lockSector,
|
|
316
|
-
});
|
|
314
|
+
const { allocTask, setSlotLength, free } = register({ lockSector });
|
|
317
315
|
const { writeBinary: writeDynamicBinary, writeBuffer: writeDynamicBuffer, writeArrayBuffer: writeDynamicArrayBuffer, write8Binary: writeDynamic8Binary, writeUtf8: writeDynamicUtf8, } = createSharedDynamicBufferIO({
|
|
318
316
|
sab: payloadSab,
|
|
319
317
|
payloadConfig: resolvedPayloadConfig,
|
|
@@ -1219,9 +1217,7 @@ export const decodePayload = ({ lockSector, payload, sab, payloadConfig, headers
|
|
|
1219
1217
|
sab: payloadSab,
|
|
1220
1218
|
options: payload?.config ?? payloadConfig,
|
|
1221
1219
|
});
|
|
1222
|
-
const { free } = register({
|
|
1223
|
-
lockSector,
|
|
1224
|
-
});
|
|
1220
|
+
const { free } = register({ lockSector });
|
|
1225
1221
|
const freeTaskSlot = (task) => free(getTaskSlotIndex(task));
|
|
1226
1222
|
const { readUtf8: readDynamicUtf8, readBytesCopy: readDynamicBytesCopy, readBytesBufferCopy: readDynamicBufferCopy, readBufferCopy: readDynamicBuffer, readBytesArrayBufferCopy: readDynamicArrayBufferCopy, readArrayBufferCopy: readDynamicArrayBuffer, read8BytesFloatCopy: readDynamic8BytesFloatCopy, read8BytesFloatView: readDynamic8BytesFloatView, } = createSharedDynamicBufferIO({
|
|
1227
1223
|
sab: payloadSab,
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import type { Task } from "./lock.js";
|
|
2
2
|
import { type SharedBufferSource } from "../common/shared-buffer-region.js";
|
|
3
3
|
export type RegisterMalloc = ReturnType<typeof register>;
|
|
4
|
-
export
|
|
5
|
-
export declare const register: ({ lockSector, publishMode, }: {
|
|
4
|
+
export declare const register: ({ lockSector, }: {
|
|
6
5
|
lockSector?: SharedBufferSource;
|
|
7
|
-
publishMode?: RegionRegistryPublishMode;
|
|
8
6
|
}) => {
|
|
9
7
|
allocTask: (task: Task) => number;
|
|
10
8
|
setSlotLength: (slotIndex: number, payloadLen: number) => boolean;
|
|
@@ -4,7 +4,7 @@ import { toSharedBufferRegion, } from "../common/shared-buffer-region.js";
|
|
|
4
4
|
// Low 5 bits = slot index, high 27 bits = caller meta.
|
|
5
5
|
// Inlined from setTaskSlotIndex to avoid cross-closure call on hot path.
|
|
6
6
|
const SLOT_META_PACKED_MASK = 0xFFFFFFE0; // (~0x1F) >>> 0
|
|
7
|
-
export const register = ({ lockSector,
|
|
7
|
+
export const register = ({ lockSector, }) => {
|
|
8
8
|
const lockRegion = toSharedBufferRegion(lockSector ?? createWasmSharedArrayBuffer(LOCK_SECTOR_BYTE_LENGTH));
|
|
9
9
|
const lockSAB = lockRegion.sab;
|
|
10
10
|
const hostBits = new Int32Array(lockSAB, lockRegion.byteOffset + PAYLOAD_LOCK_HOST_BITS_OFFSET_BYTES, 1);
|
|
@@ -14,7 +14,7 @@ export const register = ({ lockSector, publishMode = "plain", }) => {
|
|
|
14
14
|
const clz32 = Math.clz32;
|
|
15
15
|
const a_load = Atomics.load;
|
|
16
16
|
const a_store = Atomics.store;
|
|
17
|
-
const
|
|
17
|
+
const a_xor = Atomics.xor;
|
|
18
18
|
const EMPTY = 0xFFFFFFFF >>> 0;
|
|
19
19
|
const SLOT_MASK = TASK_SLOT_INDEX_MASK;
|
|
20
20
|
const START_MASK = (~SLOT_MASK) >>> 0;
|
|
@@ -23,7 +23,6 @@ export const register = ({ lockSector, publishMode = "plain", }) => {
|
|
|
23
23
|
let usedBits = 0 | 0;
|
|
24
24
|
// scalar state (faster than Uint32Array(1))
|
|
25
25
|
let hostLast = 0 | 0;
|
|
26
|
-
let workerLast = 0 | 0;
|
|
27
26
|
const startAndIndexToArray = (length) => startAndIndex.slice(0, length);
|
|
28
27
|
const compactFreeBitsStable = (b, freeBits) => {
|
|
29
28
|
const sai = startAndIndex;
|
|
@@ -92,10 +91,14 @@ export const register = ({ lockSector, publishMode = "plain", }) => {
|
|
|
92
91
|
let freeBits = (~(hostLast ^ w)) >>> 0;
|
|
93
92
|
if (freeBits !== 0)
|
|
94
93
|
freeBits &= usedBits;
|
|
95
|
-
|
|
94
|
+
// Every tracked slot is free, so restart the arena from offset 0. A free
|
|
95
|
+
// racing after this snapshot can only delay reclamation until the next
|
|
96
|
+
// allocation; it cannot make a live slot appear free.
|
|
97
|
+
if (freeBits === (usedBits >>> 0)) {
|
|
96
98
|
tableLength = 0;
|
|
97
99
|
usedBits = 0 | 0;
|
|
98
100
|
tl = 0;
|
|
101
|
+
freeBits = 0 >>> 0;
|
|
99
102
|
}
|
|
100
103
|
else if (freeBits !== 0) {
|
|
101
104
|
for (let i = 0; i < tl; i++) {
|
|
@@ -247,10 +250,7 @@ export const register = ({ lockSector, publishMode = "plain", }) => {
|
|
|
247
250
|
const slotIndex = findAndInsert(task, size);
|
|
248
251
|
if (slotIndex === -1)
|
|
249
252
|
return -1;
|
|
250
|
-
|
|
251
|
-
a_store(hostBits, 0, hostLast);
|
|
252
|
-
else
|
|
253
|
-
hostBits[0] = hostLast;
|
|
253
|
+
a_store(hostBits, 0, hostLast);
|
|
254
254
|
return slotIndex;
|
|
255
255
|
};
|
|
256
256
|
const setSlotLength = (slotIndex, payloadLen) => {
|
|
@@ -264,13 +264,15 @@ export const register = ({ lockSector, publishMode = "plain", }) => {
|
|
|
264
264
|
size64bit[slotIndex] = aligned >>> 0;
|
|
265
265
|
return true;
|
|
266
266
|
};
|
|
267
|
+
// Always a read-modify-write. `free()` genuinely has two callers even without
|
|
268
|
+
// work stealing: the encode-side rollback (`failDynamicWriteAfterReserve`)
|
|
269
|
+
// and the decode-side release (`freeTaskSlot`). Each held a private
|
|
270
|
+
// `workerLast` shadow and blind-stored the whole word, so one overwrote the
|
|
271
|
+
// other's toggles — the allocator then handed out a region that was still
|
|
272
|
+
// live, aliasing payloads. XOR of distinct bits commutes, so one atomic
|
|
273
|
+
// toggle per free is correct for any number of writers.
|
|
267
274
|
const free = (index) => {
|
|
268
|
-
|
|
269
|
-
workerLast ^= 1 << index;
|
|
270
|
-
if (useAtomicPublish)
|
|
271
|
-
a_store(workerBits, 0, workerLast);
|
|
272
|
-
else
|
|
273
|
-
workerBits[0] = workerLast;
|
|
275
|
+
a_xor(workerBits, 0, 1 << (index & TASK_SLOT_INDEX_MASK));
|
|
274
276
|
};
|
|
275
277
|
return {
|
|
276
278
|
allocTask,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RUNTIME } from "../common/runtime.js";
|
|
1
|
+
import { IS_BROWSER, RUNTIME } from "../common/runtime.js";
|
|
2
2
|
import { toCanonicalPath as toSharedCanonicalPath } from "../common/path-canonical.js";
|
|
3
3
|
import { getNodeBuiltinModule, getNodeProcess } from "../common/node-compat.js";
|
|
4
4
|
const requireNodeModule = (specifier) => {
|
|
@@ -436,6 +436,10 @@ export const resolvePermissionProtocol = ({ permission, modules, }) => {
|
|
|
436
436
|
const input = normalizeProtocolInput(permission);
|
|
437
437
|
if (!input)
|
|
438
438
|
return undefined;
|
|
439
|
+
// Browsers have no filesystem, process, or runtime flags to police, and the
|
|
440
|
+
// strict default would otherwise reach for `node:path` and throw.
|
|
441
|
+
if (IS_BROWSER)
|
|
442
|
+
return undefined;
|
|
439
443
|
const rawMode = input.mode;
|
|
440
444
|
const mode = (rawMode === "unsafe" || rawMode === "off")
|
|
441
445
|
? "unsafe"
|
|
@@ -2,12 +2,14 @@ import { type MultiQueue } from "./tx-queue.js";
|
|
|
2
2
|
import { type MainSignal } from "../ipc/transport/shared-memory.js";
|
|
3
3
|
import { type RuntimeMessageChannelLike, type RuntimeMessagePortLike } from "../common/worker-runtime.js";
|
|
4
4
|
import type { DispatcherSettings } from "../types.js";
|
|
5
|
-
export declare const hostDispatcherLoop: ({ signalBox: { opView, txStatus, rxStatus, }, queue: { completeFrame, hasPendingFrames, flushToWorker, txIdle, }, channelHandler, dispatcherOptions, notifySignal, }: {
|
|
5
|
+
export declare const hostDispatcherLoop: ({ signalBox: { opView, txStatus, rxStatus, }, queue: { completeFrame, hasPendingFrames, flushToWorker, txIdle, waitForCompletion, setCompletionWaiterArmed, }, channelHandler, dispatcherOptions, notifySignal, crossProcess, }: {
|
|
6
6
|
queue: MultiQueue;
|
|
7
7
|
signalBox: MainSignal;
|
|
8
8
|
channelHandler: ChannelHandler;
|
|
9
9
|
dispatcherOptions?: DispatcherSettings;
|
|
10
10
|
notifySignal?: () => void;
|
|
11
|
+
/** Workers live in other processes; disables the doorbell. */
|
|
12
|
+
crossProcess?: boolean;
|
|
11
13
|
}) => {
|
|
12
14
|
check: {
|
|
13
15
|
(): void;
|
|
@@ -20,20 +22,21 @@ type CheckWithState = (() => void) & {
|
|
|
20
22
|
rerun: boolean;
|
|
21
23
|
};
|
|
22
24
|
export type DispatcherCheck = CheckWithState;
|
|
25
|
+
export type ChannelHandlerPump = "auto" | "channel";
|
|
23
26
|
export declare class ChannelHandler {
|
|
24
27
|
#private;
|
|
25
|
-
channel: RuntimeMessageChannelLike;
|
|
26
|
-
port1: RuntimeMessagePortLike;
|
|
27
|
-
port2: RuntimeMessagePortLike;
|
|
28
|
-
constructor();
|
|
28
|
+
channel: RuntimeMessageChannelLike | undefined;
|
|
29
|
+
port1: RuntimeMessagePortLike | undefined;
|
|
30
|
+
port2: RuntimeMessagePortLike | undefined;
|
|
31
|
+
constructor(pump?: ChannelHandlerPump);
|
|
29
32
|
notify(): void;
|
|
30
33
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
34
|
+
* Registers the handler the pump calls back into. On the channel pump this is
|
|
35
|
+
* also where the ports are opened, so `notify` can reach port 1.
|
|
33
36
|
*/
|
|
34
37
|
open(f: () => void): void;
|
|
35
38
|
/**
|
|
36
|
-
*
|
|
39
|
+
* Detaches the handler, and closes the channel if this pump has one.
|
|
37
40
|
*/
|
|
38
41
|
close(): void;
|
|
39
42
|
}
|