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
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A payload-region registry with more than 64 identities and a lazy free set.
|
|
3
|
+
*
|
|
4
|
+
* Two independent changes from `regionRegistry.ts`:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Width.** That registry packs `start | slot` into one Uint32 and steals
|
|
7
|
+
* the low 6 bits for the identity, which is exactly the 64-slot ceiling
|
|
8
|
+
* (regions are 64-byte aligned, so only 6 bits are spare). This one unpacks
|
|
9
|
+
* the table into `tableStart` + `tableSlot`, and its host-local
|
|
10
|
+
* `usedBits0/1` scalars become an Int32Array of `slots / 32` words plus a
|
|
11
|
+
* one-bit-per-word "this word still has a free identity" mask so the
|
|
12
|
+
* free-slot search stays O(1) instead of O(words).
|
|
13
|
+
*
|
|
14
|
+
* 2. **Laziness.** That registry's `findAndInsert` calls `updateTable()` on
|
|
15
|
+
* every allocation (an atomic load per word plus an O(tableLength)
|
|
16
|
+
* compaction) and then walks the sorted table for a first fit. `lazy` mode
|
|
17
|
+
* instead borrows the sender-shadow rule from `lock.ts`: reconcile only
|
|
18
|
+
* when the cached free set is exhausted. Staleness can only mark an already-free
|
|
19
|
+
* identity as busy, never a busy one as free, so a stale read costs
|
|
20
|
+
* capacity and never correctness -- the same false-busy-only property the
|
|
21
|
+
* queue's `ensureSenderStateHasFree` relies on.
|
|
22
|
+
*
|
|
23
|
+
* Between reconciles the allocator is a bump pointer over the arena: append
|
|
24
|
+
* past the highest live region, O(1), no scan and no insert shift. Holes
|
|
25
|
+
* left by freed regions below the tail are reclaimed only when the bump
|
|
26
|
+
* window is exhausted (a reconcile, then a sorted first-fit) or when every
|
|
27
|
+
* identity has drained, which resets the arena to offset 0. That is the
|
|
28
|
+
* "old data is fine" trade: the allocator is eventually consistent with the
|
|
29
|
+
* real free set, and pays for it in arena high-water, not in correctness.
|
|
30
|
+
*/
|
|
31
|
+
/** Region alignment, matching the shipped registry. */
|
|
32
|
+
export declare const REGION_ALIGN = 64;
|
|
33
|
+
/** hostBits and workerBits each own one 64-byte line: 16 words = 512 slots. */
|
|
34
|
+
export declare const MAX_REGION_SLOTS = 512;
|
|
35
|
+
/**
|
|
36
|
+
* Largest arena this registry will manage.
|
|
37
|
+
*
|
|
38
|
+
* Offsets and sizes are folded with int32 bitwise operators (the alignment
|
|
39
|
+
* round-up, the bump-pointer compare), so an arena that reaches into the
|
|
40
|
+
* unsigned range would wrap a size negative and let it pass a bound it should
|
|
41
|
+
* have failed. Capping at 2 GiB minus one alignment unit keeps every
|
|
42
|
+
* intermediate -- `byteLength + (REGION_ALIGN - 1)` included -- inside the
|
|
43
|
+
* positive int32 range.
|
|
44
|
+
*/
|
|
45
|
+
export declare const MAX_ARENA_BYTE_LENGTH: number;
|
|
46
|
+
export type LazyRegionRegistryMode = "eager" | "lazy";
|
|
47
|
+
export type LazyRegionRegistryOptions = {
|
|
48
|
+
/** Region identities. Multiple of 32, up to `MAX_REGION_SLOTS`. */
|
|
49
|
+
slots?: number;
|
|
50
|
+
/**
|
|
51
|
+
* `eager` reproduces the shipped policy at the new width: reconcile every
|
|
52
|
+
* allocation, always sorted first-fit. `lazy` reconciles on exhaustion and
|
|
53
|
+
* bump-allocates in between.
|
|
54
|
+
*/
|
|
55
|
+
mode?: LazyRegionRegistryMode;
|
|
56
|
+
/** Bump window before the allocator must reclaim holes. */
|
|
57
|
+
arenaByteLength?: number;
|
|
58
|
+
lockSector?: SharedArrayBuffer;
|
|
59
|
+
};
|
|
60
|
+
export type LazyRegionRegistry = ReturnType<typeof createLazyRegionRegistry>;
|
|
61
|
+
export declare const createLazyRegionRegistry: ({ slots, mode, arenaByteLength, lockSector, }?: LazyRegionRegistryOptions) => {
|
|
62
|
+
allocRegion: (byteLength: number) => number;
|
|
63
|
+
regionStart: (slot: number) => number;
|
|
64
|
+
trimRegion: (slot: number, byteLength: number) => number;
|
|
65
|
+
slotContaining: (offset: number, byteLength: number) => number;
|
|
66
|
+
free: (slot: number) => void;
|
|
67
|
+
reconcile: () => boolean;
|
|
68
|
+
stats: () => {
|
|
69
|
+
slots: number;
|
|
70
|
+
mode: LazyRegionRegistryMode;
|
|
71
|
+
tableLength: number;
|
|
72
|
+
tailEnd: number;
|
|
73
|
+
highWater: number;
|
|
74
|
+
reconciles: number;
|
|
75
|
+
firstFits: number;
|
|
76
|
+
appends: number;
|
|
77
|
+
resets: number;
|
|
78
|
+
};
|
|
79
|
+
resetStats: () => void;
|
|
80
|
+
hostBits: Int32Array<SharedArrayBuffer>;
|
|
81
|
+
workerBits: Int32Array<SharedArrayBuffer>;
|
|
82
|
+
lockSAB: SharedArrayBuffer;
|
|
83
|
+
};
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A payload-region registry with more than 64 identities and a lazy free set.
|
|
3
|
+
*
|
|
4
|
+
* Two independent changes from `regionRegistry.ts`:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Width.** That registry packs `start | slot` into one Uint32 and steals
|
|
7
|
+
* the low 6 bits for the identity, which is exactly the 64-slot ceiling
|
|
8
|
+
* (regions are 64-byte aligned, so only 6 bits are spare). This one unpacks
|
|
9
|
+
* the table into `tableStart` + `tableSlot`, and its host-local
|
|
10
|
+
* `usedBits0/1` scalars become an Int32Array of `slots / 32` words plus a
|
|
11
|
+
* one-bit-per-word "this word still has a free identity" mask so the
|
|
12
|
+
* free-slot search stays O(1) instead of O(words).
|
|
13
|
+
*
|
|
14
|
+
* 2. **Laziness.** That registry's `findAndInsert` calls `updateTable()` on
|
|
15
|
+
* every allocation (an atomic load per word plus an O(tableLength)
|
|
16
|
+
* compaction) and then walks the sorted table for a first fit. `lazy` mode
|
|
17
|
+
* instead borrows the sender-shadow rule from `lock.ts`: reconcile only
|
|
18
|
+
* when the cached free set is exhausted. Staleness can only mark an already-free
|
|
19
|
+
* identity as busy, never a busy one as free, so a stale read costs
|
|
20
|
+
* capacity and never correctness -- the same false-busy-only property the
|
|
21
|
+
* queue's `ensureSenderStateHasFree` relies on.
|
|
22
|
+
*
|
|
23
|
+
* Between reconciles the allocator is a bump pointer over the arena: append
|
|
24
|
+
* past the highest live region, O(1), no scan and no insert shift. Holes
|
|
25
|
+
* left by freed regions below the tail are reclaimed only when the bump
|
|
26
|
+
* window is exhausted (a reconcile, then a sorted first-fit) or when every
|
|
27
|
+
* identity has drained, which resets the arena to offset 0. That is the
|
|
28
|
+
* "old data is fine" trade: the allocator is eventually consistent with the
|
|
29
|
+
* real free set, and pays for it in arena high-water, not in correctness.
|
|
30
|
+
*/
|
|
31
|
+
import { LOCK_SECTOR_BYTE_LENGTH, PAYLOAD_LOCK_HOST_BITS_OFFSET_BYTES, PAYLOAD_LOCK_WORKER_BITS_OFFSET_BYTES, } from "./lock.js";
|
|
32
|
+
/** Region alignment, matching the shipped registry. */
|
|
33
|
+
export const REGION_ALIGN = 64;
|
|
34
|
+
/** hostBits and workerBits each own one 64-byte line: 16 words = 512 slots. */
|
|
35
|
+
export const MAX_REGION_SLOTS = 512;
|
|
36
|
+
/**
|
|
37
|
+
* Largest arena this registry will manage.
|
|
38
|
+
*
|
|
39
|
+
* Offsets and sizes are folded with int32 bitwise operators (the alignment
|
|
40
|
+
* round-up, the bump-pointer compare), so an arena that reaches into the
|
|
41
|
+
* unsigned range would wrap a size negative and let it pass a bound it should
|
|
42
|
+
* have failed. Capping at 2 GiB minus one alignment unit keeps every
|
|
43
|
+
* intermediate -- `byteLength + (REGION_ALIGN - 1)` included -- inside the
|
|
44
|
+
* positive int32 range.
|
|
45
|
+
*/
|
|
46
|
+
export const MAX_ARENA_BYTE_LENGTH = 0x7fffffff - (REGION_ALIGN - 1);
|
|
47
|
+
const clz32 = Math.clz32;
|
|
48
|
+
const a_load = Atomics.load;
|
|
49
|
+
const a_store = Atomics.store;
|
|
50
|
+
const a_xor = Atomics.xor;
|
|
51
|
+
/** Index of the lowest set bit. */
|
|
52
|
+
const ctz = (word) => 31 - clz32((word & -word) >>> 0);
|
|
53
|
+
export const createLazyRegionRegistry = ({ slots = 128, mode = "lazy", arenaByteLength = 64 * 1024 * 1024, lockSector, } = {}) => {
|
|
54
|
+
if (slots < 32 || slots > MAX_REGION_SLOTS || (slots & 31) !== 0) {
|
|
55
|
+
throw new RangeError(`slots must be a multiple of 32 in [32, ${MAX_REGION_SLOTS}]`);
|
|
56
|
+
}
|
|
57
|
+
if (!Number.isSafeInteger(arenaByteLength) || arenaByteLength < REGION_ALIGN ||
|
|
58
|
+
arenaByteLength > MAX_ARENA_BYTE_LENGTH) {
|
|
59
|
+
throw new RangeError(`arenaByteLength must be an integer in ` +
|
|
60
|
+
`[${REGION_ALIGN}, ${MAX_ARENA_BYTE_LENGTH}]`);
|
|
61
|
+
}
|
|
62
|
+
const words = slots >>> 5;
|
|
63
|
+
const lazy = mode === "lazy";
|
|
64
|
+
/**
|
|
65
|
+
* Identities are addressed by index, never by mask: `slots` is only required
|
|
66
|
+
* to be a multiple of 32, so `slot & (slots - 1)` is a valid reduction only
|
|
67
|
+
* when `slots` happens to be a power of two. At `slots: 96` it folded slot 32
|
|
68
|
+
* onto slot 0, handing two live regions the same offset and the same release
|
|
69
|
+
* bit. Callers pass identities this registry itself handed out, so an
|
|
70
|
+
* out-of-range one is a bug worth surfacing rather than silently aliasing.
|
|
71
|
+
*/
|
|
72
|
+
const assertSlot = (slot) => {
|
|
73
|
+
if (!Number.isSafeInteger(slot) || slot < 0 || slot >= slots) {
|
|
74
|
+
throw new RangeError(`region identity ${slot} outside 0..${slots - 1}`);
|
|
75
|
+
}
|
|
76
|
+
return slot;
|
|
77
|
+
};
|
|
78
|
+
const lockSAB = lockSector ?? new SharedArrayBuffer(LOCK_SECTOR_BYTE_LENGTH);
|
|
79
|
+
const hostBits = new Int32Array(lockSAB, PAYLOAD_LOCK_HOST_BITS_OFFSET_BYTES, words);
|
|
80
|
+
const workerBits = new Int32Array(lockSAB, PAYLOAD_LOCK_WORKER_BITS_OFFSET_BYTES, words);
|
|
81
|
+
// Host-local shadows. `used` is the host's belief about live identities;
|
|
82
|
+
// `hostLast` mirrors what the host has published into `hostBits`.
|
|
83
|
+
const used = new Int32Array(words);
|
|
84
|
+
const hostLast = new Int32Array(words);
|
|
85
|
+
const freedScratch = new Int32Array(words);
|
|
86
|
+
// One bit per word: set while that word still holds a free identity.
|
|
87
|
+
const ALL_WORDS = words === 32 ? -1 : ((1 << words) - 1) | 0;
|
|
88
|
+
let freeWordMask = ALL_WORDS;
|
|
89
|
+
// Extent table, sorted by offset, unpacked so the identity is not confined
|
|
90
|
+
// to the spare alignment bits of the offset.
|
|
91
|
+
const tableStart = new Uint32Array(slots);
|
|
92
|
+
const tableSlot = new Uint16Array(slots);
|
|
93
|
+
let tableLength = 0;
|
|
94
|
+
const startBySlot = new Uint32Array(slots);
|
|
95
|
+
const sizeBySlot = new Uint32Array(slots);
|
|
96
|
+
/** End of the highest live region: the bump pointer. */
|
|
97
|
+
let tailEnd = 0;
|
|
98
|
+
let highWater = 0;
|
|
99
|
+
let reconciles = 0;
|
|
100
|
+
let firstFits = 0;
|
|
101
|
+
let resets = 0;
|
|
102
|
+
let appends = 0;
|
|
103
|
+
const findFreeSlot = () => {
|
|
104
|
+
let mask = freeWordMask;
|
|
105
|
+
while (mask !== 0) {
|
|
106
|
+
const w = ctz(mask);
|
|
107
|
+
const available = ~used[w];
|
|
108
|
+
if (available !== 0) {
|
|
109
|
+
const slot = (w << 5) + ctz(available);
|
|
110
|
+
return slot < slots ? slot : -1;
|
|
111
|
+
}
|
|
112
|
+
freeWordMask = (freeWordMask & ~(1 << w)) | 0;
|
|
113
|
+
mask = freeWordMask;
|
|
114
|
+
}
|
|
115
|
+
return -1;
|
|
116
|
+
};
|
|
117
|
+
const reserveSlot = (slot) => {
|
|
118
|
+
const w = slot >>> 5;
|
|
119
|
+
const bit = (1 << (slot & 31)) | 0;
|
|
120
|
+
used[w] = (used[w] | bit) | 0;
|
|
121
|
+
hostLast[w] = (hostLast[w] ^ bit) | 0;
|
|
122
|
+
a_store(hostBits, w, hostLast[w]);
|
|
123
|
+
if (~used[w] === 0)
|
|
124
|
+
freeWordMask = (freeWordMask & ~(1 << w)) | 0;
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* Fold the consumer's release toggles into the host shadow. Returns true
|
|
128
|
+
* when anything was reclaimed. This is the only place that reads the shared
|
|
129
|
+
* words, so it is the only cost laziness removes from the alloc path.
|
|
130
|
+
*/
|
|
131
|
+
const reconcile = () => {
|
|
132
|
+
reconciles++;
|
|
133
|
+
let anyFreed = 0;
|
|
134
|
+
let stillUsed = 0;
|
|
135
|
+
for (let w = 0; w < words; w++) {
|
|
136
|
+
const live = used[w];
|
|
137
|
+
if (live === 0) {
|
|
138
|
+
freedScratch[w] = 0;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const freed = (~(hostLast[w] ^ a_load(workerBits, w)) & live) | 0;
|
|
142
|
+
freedScratch[w] = freed;
|
|
143
|
+
anyFreed |= freed;
|
|
144
|
+
stillUsed |= (live & ~freed) | 0;
|
|
145
|
+
}
|
|
146
|
+
if (anyFreed === 0)
|
|
147
|
+
return false;
|
|
148
|
+
// Every identity drained: reset the arena instead of compacting.
|
|
149
|
+
if (stillUsed === 0) {
|
|
150
|
+
resets++;
|
|
151
|
+
tableLength = 0;
|
|
152
|
+
tailEnd = 0;
|
|
153
|
+
used.fill(0);
|
|
154
|
+
freeWordMask = ALL_WORDS;
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
for (let w = 0; w < words; w++) {
|
|
158
|
+
const freed = freedScratch[w];
|
|
159
|
+
if (freed === 0)
|
|
160
|
+
continue;
|
|
161
|
+
used[w] = (used[w] & ~freed) | 0;
|
|
162
|
+
freeWordMask = (freeWordMask | (1 << w)) | 0;
|
|
163
|
+
}
|
|
164
|
+
let write = 0;
|
|
165
|
+
for (let read = 0; read < tableLength; read++) {
|
|
166
|
+
const slot = tableSlot[read];
|
|
167
|
+
if ((freedScratch[slot >>> 5] & (1 << (slot & 31))) !== 0)
|
|
168
|
+
continue;
|
|
169
|
+
if (write !== read) {
|
|
170
|
+
tableStart[write] = tableStart[read];
|
|
171
|
+
tableSlot[write] = slot;
|
|
172
|
+
}
|
|
173
|
+
write++;
|
|
174
|
+
}
|
|
175
|
+
tableLength = write;
|
|
176
|
+
// Regions never overlap, so sorted by start is sorted by end.
|
|
177
|
+
tailEnd = write === 0
|
|
178
|
+
? 0
|
|
179
|
+
: (tableStart[write - 1] + sizeBySlot[tableSlot[write - 1]]) >>> 0;
|
|
180
|
+
return true;
|
|
181
|
+
};
|
|
182
|
+
/** O(1) append past the highest live region. */
|
|
183
|
+
const append = (slot, size) => {
|
|
184
|
+
appends++;
|
|
185
|
+
const start = tailEnd;
|
|
186
|
+
tableStart[tableLength] = start;
|
|
187
|
+
tableSlot[tableLength] = slot;
|
|
188
|
+
tableLength++;
|
|
189
|
+
tailEnd = start + size;
|
|
190
|
+
if (tailEnd > highWater)
|
|
191
|
+
highWater = tailEnd;
|
|
192
|
+
startBySlot[slot] = start;
|
|
193
|
+
sizeBySlot[slot] = size;
|
|
194
|
+
reserveSlot(slot);
|
|
195
|
+
return slot;
|
|
196
|
+
};
|
|
197
|
+
/** Sorted first fit over the holes left below the tail. */
|
|
198
|
+
const firstFit = (slot, size) => {
|
|
199
|
+
firstFits++;
|
|
200
|
+
let previousEnd = 0;
|
|
201
|
+
let insertAt = tableLength;
|
|
202
|
+
for (let at = 0; at < tableLength; at++) {
|
|
203
|
+
const start = tableStart[at];
|
|
204
|
+
if (((start - previousEnd) >>> 0) >= (size >>> 0)) {
|
|
205
|
+
insertAt = at;
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
previousEnd = (start + sizeBySlot[tableSlot[at]]) >>> 0;
|
|
209
|
+
}
|
|
210
|
+
if (previousEnd + size > arenaByteLength)
|
|
211
|
+
return -1;
|
|
212
|
+
for (let at = tableLength; at > insertAt; at--) {
|
|
213
|
+
tableStart[at] = tableStart[at - 1];
|
|
214
|
+
tableSlot[at] = tableSlot[at - 1];
|
|
215
|
+
}
|
|
216
|
+
tableStart[insertAt] = previousEnd;
|
|
217
|
+
tableSlot[insertAt] = slot;
|
|
218
|
+
tableLength++;
|
|
219
|
+
startBySlot[slot] = previousEnd;
|
|
220
|
+
sizeBySlot[slot] = size;
|
|
221
|
+
const end = previousEnd + size;
|
|
222
|
+
if (end > tailEnd)
|
|
223
|
+
tailEnd = end;
|
|
224
|
+
if (end > highWater)
|
|
225
|
+
highWater = end;
|
|
226
|
+
reserveSlot(slot);
|
|
227
|
+
return slot;
|
|
228
|
+
};
|
|
229
|
+
const allocRegion = (byteLength) => {
|
|
230
|
+
// `byteLength | 0` truncated instead of rejecting: a 2 GiB request wrapped
|
|
231
|
+
// to a negative size, passed the arena bound it should have failed, and
|
|
232
|
+
// left `tailEnd` negative for the life of the pool. Anything the arena
|
|
233
|
+
// cannot hold is -1, which is the caller's signal to take the overflow
|
|
234
|
+
// path -- the same answer it already gets when identities run out.
|
|
235
|
+
if (!Number.isSafeInteger(byteLength) || byteLength < 0 ||
|
|
236
|
+
byteLength > arenaByteLength) {
|
|
237
|
+
return -1;
|
|
238
|
+
}
|
|
239
|
+
const size = (byteLength + (REGION_ALIGN - 1)) & ~(REGION_ALIGN - 1);
|
|
240
|
+
if (!lazy)
|
|
241
|
+
reconcile();
|
|
242
|
+
let slot = findFreeSlot();
|
|
243
|
+
if (slot === -1) {
|
|
244
|
+
if (!reconcile())
|
|
245
|
+
return -1;
|
|
246
|
+
slot = findFreeSlot();
|
|
247
|
+
if (slot === -1)
|
|
248
|
+
return -1;
|
|
249
|
+
}
|
|
250
|
+
if (tableLength >= slots || (tailEnd + size) > arenaByteLength) {
|
|
251
|
+
reconcile();
|
|
252
|
+
slot = findFreeSlot();
|
|
253
|
+
if (slot === -1 || tableLength >= slots)
|
|
254
|
+
return -1;
|
|
255
|
+
}
|
|
256
|
+
if (lazy && (tailEnd + size) <= arenaByteLength)
|
|
257
|
+
return append(slot, size);
|
|
258
|
+
return firstFit(slot, size);
|
|
259
|
+
};
|
|
260
|
+
const regionStart = (slot) => startBySlot[assertSlot(slot)];
|
|
261
|
+
/**
|
|
262
|
+
* Shrink a region to `byteLength`, returning its new aligned size.
|
|
263
|
+
*
|
|
264
|
+
* This is what makes a streamed body of unknown length affordable: reserve
|
|
265
|
+
* an upper bound, fill it, then give back what was not used. When the region
|
|
266
|
+
* is the tail allocation -- the common case, since it was just bump
|
|
267
|
+
* allocated -- the bump pointer rewinds and nothing is wasted at all.
|
|
268
|
+
* Otherwise the tail of the region simply becomes a hole, reclaimed on the
|
|
269
|
+
* next reconcile like any other.
|
|
270
|
+
*
|
|
271
|
+
* Growing is not offered: it would have to relocate the bytes, which is the
|
|
272
|
+
* copy the caller is trying to avoid.
|
|
273
|
+
*/
|
|
274
|
+
const trimRegion = (slot, byteLength) => {
|
|
275
|
+
const index = assertSlot(slot);
|
|
276
|
+
const current = sizeBySlot[index];
|
|
277
|
+
if (!Number.isSafeInteger(byteLength) || byteLength < 0)
|
|
278
|
+
return current;
|
|
279
|
+
const size = (byteLength + (REGION_ALIGN - 1)) & ~(REGION_ALIGN - 1);
|
|
280
|
+
if (size >= current)
|
|
281
|
+
return current;
|
|
282
|
+
const start = startBySlot[index];
|
|
283
|
+
if (start + current === tailEnd)
|
|
284
|
+
tailEnd = start + size;
|
|
285
|
+
sizeBySlot[index] = size;
|
|
286
|
+
return size;
|
|
287
|
+
};
|
|
288
|
+
/**
|
|
289
|
+
* The identity whose extent contains [offset, offset + byteLength), or -1.
|
|
290
|
+
* Binary search over the offset-sorted extent table: exact, no side table to
|
|
291
|
+
* maintain, and it accepts interior offsets so a subarray of a borrowed view
|
|
292
|
+
* still resolves to its region.
|
|
293
|
+
*
|
|
294
|
+
* Under lazy mode the table can still list regions the consumer has already
|
|
295
|
+
* released but the owner has not reconciled. That is deliberate: those bytes
|
|
296
|
+
* are not reusable until the owner observes the release, so a view into one
|
|
297
|
+
* is still valid.
|
|
298
|
+
*/
|
|
299
|
+
const slotContaining = (offset, byteLength) => {
|
|
300
|
+
let lo = 0;
|
|
301
|
+
let hi = tableLength - 1;
|
|
302
|
+
while (lo <= hi) {
|
|
303
|
+
const mid = (lo + hi) >>> 1;
|
|
304
|
+
const start = tableStart[mid];
|
|
305
|
+
if (offset < start) {
|
|
306
|
+
hi = mid - 1;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
const slot = tableSlot[mid];
|
|
310
|
+
const end = start + sizeBySlot[slot];
|
|
311
|
+
if (offset >= end) {
|
|
312
|
+
lo = mid + 1;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
// Starts inside this region: it fits, or it runs past the extent.
|
|
316
|
+
return offset + byteLength <= end ? slot : -1;
|
|
317
|
+
}
|
|
318
|
+
return -1;
|
|
319
|
+
};
|
|
320
|
+
/** Consumer-side release: one XOR toggle per region, as shipped. */
|
|
321
|
+
const free = (slot) => {
|
|
322
|
+
const s = assertSlot(slot);
|
|
323
|
+
a_xor(workerBits, s >>> 5, (1 << (s & 31)) | 0);
|
|
324
|
+
};
|
|
325
|
+
const stats = () => ({
|
|
326
|
+
slots,
|
|
327
|
+
mode,
|
|
328
|
+
tableLength,
|
|
329
|
+
tailEnd,
|
|
330
|
+
highWater,
|
|
331
|
+
reconciles,
|
|
332
|
+
firstFits,
|
|
333
|
+
appends,
|
|
334
|
+
resets,
|
|
335
|
+
});
|
|
336
|
+
const resetStats = () => {
|
|
337
|
+
reconciles = 0;
|
|
338
|
+
firstFits = 0;
|
|
339
|
+
appends = 0;
|
|
340
|
+
resets = 0;
|
|
341
|
+
};
|
|
342
|
+
return {
|
|
343
|
+
allocRegion,
|
|
344
|
+
regionStart,
|
|
345
|
+
trimRegion,
|
|
346
|
+
slotContaining,
|
|
347
|
+
free,
|
|
348
|
+
reconcile,
|
|
349
|
+
stats,
|
|
350
|
+
resetStats,
|
|
351
|
+
hostBits,
|
|
352
|
+
workerBits,
|
|
353
|
+
lockSAB,
|
|
354
|
+
};
|
|
355
|
+
};
|
package/src/memory/lock.d.ts
CHANGED
|
@@ -5,9 +5,6 @@ export declare const registerLockPayloadCodec: (encode: EncodePayloadFactory, de
|
|
|
5
5
|
import { type SharedBufferSource } from "../common/shared-buffer-region.js";
|
|
6
6
|
import { type LockBufferTextCompat } from "../common/shared-buffer-text.js";
|
|
7
7
|
import { type PayloadBufferOptions } from "./payload-config.js";
|
|
8
|
-
/**
|
|
9
|
-
* TODO: Compose all the instance where the array is passed as argument
|
|
10
|
-
*/
|
|
11
8
|
export declare const PayloadSignal: {
|
|
12
9
|
readonly UNREACHABLE: 0;
|
|
13
10
|
readonly BigInt: 2;
|
|
@@ -62,6 +59,12 @@ export declare const PayloadBuffer: {
|
|
|
62
59
|
readonly EnvelopeDynamicHeaderStringExternal: 52;
|
|
63
60
|
readonly NumericArray: 53;
|
|
64
61
|
readonly StaticNumericArray: 54;
|
|
62
|
+
/** Binary payload already stored in the dynamic arena. */
|
|
63
|
+
readonly ArenaBinary: 55;
|
|
64
|
+
/** A returned Uint8Array whose ArrayBuffer ownership was moved to the host. */
|
|
65
|
+
readonly MovedBinary: 56;
|
|
66
|
+
/** A returned ArrayBuffer whose ownership was moved to the host. */
|
|
67
|
+
readonly MovedArrayBuffer: 57;
|
|
65
68
|
};
|
|
66
69
|
export type PayloadBuffer = typeof PayloadBuffer[keyof typeof PayloadBuffer];
|
|
67
70
|
export declare const payloadBufferName: (value: number) => string;
|
|
@@ -97,6 +100,8 @@ export declare const finishPromisePayload: (task: Task) => void;
|
|
|
97
100
|
export declare const isPromisePayloadPending: (task: Task) => boolean;
|
|
98
101
|
export declare const resetTaskLocalFlags: (task: Task) => void;
|
|
99
102
|
export declare const addTaskFinalizer: (task: Task, finalizer: () => void) => void;
|
|
103
|
+
/** Take an owner-lifetime transport hold, if the value supports one. */
|
|
104
|
+
export declare const takePayloadTransportHold: (value: unknown) => (() => void) | undefined;
|
|
100
105
|
export declare const attachPayloadTransportFinalizer: (task: Task, value: unknown) => void;
|
|
101
106
|
export declare const runTaskFinalizers: (task: Task) => void;
|
|
102
107
|
export declare const TaskIndex: {
|
|
@@ -107,18 +112,19 @@ export declare const TaskIndex: {
|
|
|
107
112
|
/**
|
|
108
113
|
* Host -> worker request function id (low 16 bits).
|
|
109
114
|
* High 16 bits are reserved for caller metadata on request path.
|
|
110
|
-
*
|
|
115
|
+
* Shares storage with `FlagsToHost`.
|
|
111
116
|
*/
|
|
112
117
|
readonly FunctionID: 0;
|
|
113
118
|
readonly ID: 1;
|
|
114
119
|
readonly Type: 2;
|
|
115
120
|
readonly Start: 3;
|
|
116
|
-
readonly End: 4;
|
|
117
|
-
readonly PayloadLen: 5;
|
|
118
121
|
/**
|
|
119
|
-
*
|
|
120
|
-
*
|
|
122
|
+
* Payload length; bit 31 stores the high bit of a dynamic-region index.
|
|
123
|
+
* Write the length before `tagTaskSlot()` and mask bit 31 when reading it.
|
|
121
124
|
*/
|
|
125
|
+
readonly End: 4;
|
|
126
|
+
readonly PayloadLen: 5;
|
|
127
|
+
/** Low 5 bits hold the dynamic-region index; the remaining bits hold metadata. */
|
|
122
128
|
readonly slotBuffer: 6;
|
|
123
129
|
readonly Size: 8;
|
|
124
130
|
/**
|
|
@@ -175,6 +181,8 @@ export declare const STEAL_PAYLOAD_ACK_SLOT_OFFSET_U32: number;
|
|
|
175
181
|
export declare const STEAL_LIVE_SLOT_OFFSET_U32: number;
|
|
176
182
|
/** Host-owned arm word for the return-lock completion doorbell. */
|
|
177
183
|
export declare const DOORBELL_ARMED_SLOT_OFFSET_U32: number;
|
|
184
|
+
/** Shared region-owner mask for the `cas-mask` claim discipline. */
|
|
185
|
+
export declare const STEAL_CLAIM_MASK_SLOT_OFFSET_U32: number;
|
|
178
186
|
export declare const HEADER_U32_LENGTH: number;
|
|
179
187
|
export declare const HEADER_BYTE_LENGTH: number;
|
|
180
188
|
export declare const makeTask: () => Task;
|
|
@@ -184,21 +192,20 @@ type ResolveHostOptions = {
|
|
|
184
192
|
shouldSettle?: (task: Task) => boolean;
|
|
185
193
|
activeRejectPlaceholder?: Task["reject"];
|
|
186
194
|
};
|
|
195
|
+
export type Lock2 = ReturnType<typeof lock2>;
|
|
187
196
|
/**
|
|
188
|
-
*
|
|
197
|
+
* Region mutual-exclusion discipline for stealing consumers.
|
|
189
198
|
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
* - Shared buffers must be the same between host/worker.
|
|
193
|
-
* - encode/decode are not re-entrant; payload codec uses a shared scratch buffer.
|
|
199
|
+
* - `dekker`: per-consumer intent words and an O(N) peer survey.
|
|
200
|
+
* - `cas-mask`: one shared owner mask for all regions.
|
|
194
201
|
*/
|
|
195
|
-
export type
|
|
202
|
+
export type StealClaimDiscipline = "dekker" | "cas-mask";
|
|
196
203
|
export type WaitAsyncState = "not-equal" | "ok" | "timed-out";
|
|
197
204
|
export type WaitAsyncResult = {
|
|
198
205
|
async: boolean;
|
|
199
206
|
value: WaitAsyncState | PromiseLike<WaitAsyncState>;
|
|
200
207
|
};
|
|
201
|
-
export declare const lock2: ({ headers, headerSlotStrideU32, LockBoundSector, payload, payloadConfig, payloadSector, textCompat, resultList, toSentList, recycleList, processBoundary, consumers, consumerId, regionLanes, notifyOnHostPublish, }: {
|
|
208
|
+
export declare const lock2: ({ headers, headerSlotStrideU32, LockBoundSector, payload, payloadConfig, payloadSector, textCompat, resultList, toSentList, recycleList, processBoundary, sharedReturn, moveReturn, consumers, consumerId, regionLanes, stealClaim, notifyOnHostPublish, notifyHostPublish, }: {
|
|
202
209
|
headers?: SharedBufferSource;
|
|
203
210
|
headerSlotStrideU32?: number;
|
|
204
211
|
LockBoundSector?: SharedBufferSource;
|
|
@@ -210,6 +217,17 @@ export declare const lock2: ({ headers, headerSlotStrideU32, LockBoundSector, pa
|
|
|
210
217
|
resultList?: RingQueue<Task>;
|
|
211
218
|
recycleList?: RingQueue<Task>;
|
|
212
219
|
processBoundary?: boolean;
|
|
220
|
+
/**
|
|
221
|
+
* Hand large returns to the consumer as a borrowed region instead of copying
|
|
222
|
+
* them out. Set only on a worker's return lane.
|
|
223
|
+
*/
|
|
224
|
+
sharedReturn?: boolean;
|
|
225
|
+
/**
|
|
226
|
+
* Move large top-level ArrayBuffer and Uint8Array returns to the host.
|
|
227
|
+
* Unlike `sharedReturn`, the host owns the result and it never expires.
|
|
228
|
+
* Set only on a thread worker's return lane.
|
|
229
|
+
*/
|
|
230
|
+
moveReturn?: boolean;
|
|
213
231
|
/**
|
|
214
232
|
* Number of consumer endpoints sharing this lock. `1` (default) keeps the
|
|
215
233
|
* classic single-consumer path. `> 1` enables region-Dekker work stealing and
|
|
@@ -220,8 +238,12 @@ export declare const lock2: ({ headers, headerSlotStrideU32, LockBoundSector, pa
|
|
|
220
238
|
consumerId?: number;
|
|
221
239
|
/** Lanes claimed per Dekker handshake. Paper rule: `slots / regionLanes >= consumers + 1`. */
|
|
222
240
|
regionLanes?: number;
|
|
241
|
+
/** Mutual-exclusion discipline for region claiming. */
|
|
242
|
+
stealClaim?: StealClaimDiscipline;
|
|
223
243
|
/** Notify a host-side wait after this endpoint publishes a frame. */
|
|
224
244
|
notifyOnHostPublish?: boolean;
|
|
245
|
+
/** Runtime-native host wake used when Atomics.waitAsync cannot wake it. */
|
|
246
|
+
notifyHostPublish?: () => void;
|
|
225
247
|
}) => {
|
|
226
248
|
enlist: (task: Task) => true;
|
|
227
249
|
encode: (task: Task, state?: number) => boolean;
|
|
@@ -237,6 +259,7 @@ export declare const lock2: ({ headers, headerSlotStrideU32, LockBoundSector, pa
|
|
|
237
259
|
recyclecList: RingQueue<Task>;
|
|
238
260
|
resolveHost: ({ queue, onResolved, shouldSettle, activeRejectPlaceholder, }: ResolveHostOptions) => () => number;
|
|
239
261
|
waitForHostChange: (timeoutMs?: number) => WaitAsyncResult | undefined;
|
|
262
|
+
armHostNotifier: () => boolean;
|
|
240
263
|
setHostWaiterArmed: (armed: boolean) => void;
|
|
241
264
|
hasPendingFrames: () => boolean;
|
|
242
265
|
getPendingFrameCount: () => number;
|