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,695 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `KnittingSharedBuffer` -- a thread-level pool of shared byte
|
|
3
|
+
* regions whose handle mimics `SharedArrayBuffer` rather than `Buffer`.
|
|
4
|
+
*
|
|
5
|
+
* Shape:
|
|
6
|
+
*
|
|
7
|
+
* - Each thread owns one pool: its own arena SAB, its own lock sector, its
|
|
8
|
+
* own identity space. Nothing is shared between pools except the SABs a
|
|
9
|
+
* descriptor points at.
|
|
10
|
+
* - `alloc(n)` takes a region from the lazy registry
|
|
11
|
+
* (`lazy-region-registry.ts`). Identity exhaustion is not a failure: it
|
|
12
|
+
* falls back to a standalone `SharedArrayBuffer`, so the pool never blocks
|
|
13
|
+
* and never evicts a live borrow.
|
|
14
|
+
* - The **region** owns the bytes. Views are minted from it (`u8()`,
|
|
15
|
+
* `view(Ctor)`) and are plain, non-owning typed arrays. That is the same
|
|
16
|
+
* split JS already has between a `SharedArrayBuffer` and its views, and it
|
|
17
|
+
* is what keeps a `Float64Array` over the region from being silently
|
|
18
|
+
* orphaned when some unrelated `Uint8Array` over it is dropped.
|
|
19
|
+
* - The wire form is a descriptor, never bytes. A consumer thread attaches
|
|
20
|
+
* to the producer's SABs once and materializes its own region handle.
|
|
21
|
+
* - Release is one XOR into the owning lane's shared word, from whichever
|
|
22
|
+
* thread holds the last handle.
|
|
23
|
+
*
|
|
24
|
+
* Why GC-driven release is safe here, given the lazy registry:
|
|
25
|
+
*
|
|
26
|
+
* An identity is reusable only after the owner has *observed* its release
|
|
27
|
+
* toggle (`reconcile` clears the used bit only when hostLast ^ workerBits
|
|
28
|
+
* agree). So an identity cannot be handed out again while a release for it
|
|
29
|
+
* is still outstanding, which means a late release can never apply to a
|
|
30
|
+
* newer generation -- there is no ABA to protect against with a generation
|
|
31
|
+
* counter. A forgotten handle costs one identity until the collector runs;
|
|
32
|
+
* it cannot corrupt a live region. That degradation is capacity-only, and
|
|
33
|
+
* the standalone-SAB fallback is what keeps it from turning into a stall.
|
|
34
|
+
*
|
|
35
|
+
* That argument holds for exactly one releaser per identity, which is why
|
|
36
|
+
* sending a region is `moveTo()` and not `describe()`. Two releasers
|
|
37
|
+
* reintroduce the ABA the toggle cannot see: the first release lets
|
|
38
|
+
* `reconcile` recycle the identity into a new region, and the second one
|
|
39
|
+
* then frees a live stranger, whose bytes the next `alloc` hands out while
|
|
40
|
+
* it is still being read. `describe()` is inspection only.
|
|
41
|
+
*
|
|
42
|
+
* What it still cannot do: revoke a view that was already minted. Minting is
|
|
43
|
+
* checked -- `u8()` after release throws instead of handing back a window onto
|
|
44
|
+
* somebody else's recycled bytes -- but a view handed out earlier and retained
|
|
45
|
+
* past release keeps aliasing the region. That is a JS limitation, not a
|
|
46
|
+
* design choice, and it is why `copy()` exists.
|
|
47
|
+
*/
|
|
48
|
+
import { createLazyRegionRegistry, } from "./lazy-region-registry.js";
|
|
49
|
+
import { LOCK_SECTOR_BYTE_LENGTH, PAYLOAD_LOCK_WORKER_BITS_OFFSET_BYTES, PayloadTransportFinalizer, takePayloadTransportHold, } from "./lock.js";
|
|
50
|
+
import { readBodyOrRefer, } from "./knitting-buffer-http.js";
|
|
51
|
+
export const KNITTING_BUFFER_CODEC = "knitting.buffer";
|
|
52
|
+
/**
|
|
53
|
+
* Regions may only be minted by an allocator or by adopting a descriptor.
|
|
54
|
+
* Without this, user code could fabricate a region over any buffer with any
|
|
55
|
+
* identity, and releasing it would XOR an identity it does not own.
|
|
56
|
+
*/
|
|
57
|
+
const MINT = Symbol("knitting.sharedBuffer.mint");
|
|
58
|
+
/**
|
|
59
|
+
* Module-internal ownership transfer. Reachable only through the allocator's
|
|
60
|
+
* `moveTo()`, which is the one place a region legitimately stops being ours.
|
|
61
|
+
*/
|
|
62
|
+
const DISOWN = Symbol("knitting.sharedBuffer.disown");
|
|
63
|
+
const hasSharedArrayBuffer = typeof SharedArrayBuffer === "function";
|
|
64
|
+
/**
|
|
65
|
+
* True for a buffer that may back a standalone region, including one that has
|
|
66
|
+
* crossed a transport.
|
|
67
|
+
*
|
|
68
|
+
* The check has to admit a plain `ArrayBuffer`. knitting ships a
|
|
69
|
+
* SharedArrayBuffer by pointer and rebuilds it on the far side branded as an
|
|
70
|
+
* ArrayBuffer, so `instanceof SharedArrayBuffer` is false there even though the
|
|
71
|
+
* memory is genuinely shared.
|
|
72
|
+
*/
|
|
73
|
+
const isTransportedBuffer = (value) => (hasSharedArrayBuffer && value instanceof SharedArrayBuffer) ||
|
|
74
|
+
value instanceof ArrayBuffer;
|
|
75
|
+
/**
|
|
76
|
+
* An owned region of shared memory. Mints views; does not pretend to be one.
|
|
77
|
+
*
|
|
78
|
+
* Exported for `instanceof` and for typing. It is not constructible: regions
|
|
79
|
+
* come from `createKnittingAllocator().alloc()` or from adopting a descriptor.
|
|
80
|
+
*/
|
|
81
|
+
export class KnittingSharedBuffer {
|
|
82
|
+
#buffer;
|
|
83
|
+
#byteOffset;
|
|
84
|
+
#byteLength;
|
|
85
|
+
#lane;
|
|
86
|
+
#slot;
|
|
87
|
+
// Takes `free`: true frees the identity, false surrenders it to a consumer.
|
|
88
|
+
// Both paths must unregister the collector backstop, and only one of them
|
|
89
|
+
// may toggle the shared release word.
|
|
90
|
+
#release;
|
|
91
|
+
#trim;
|
|
92
|
+
#released = false;
|
|
93
|
+
#moved = false;
|
|
94
|
+
// The u8 view is the common case and gets its own field; anything else goes
|
|
95
|
+
// in a map that is only allocated when a second view type is asked for.
|
|
96
|
+
#u8;
|
|
97
|
+
#views;
|
|
98
|
+
constructor(mint, buffer, byteOffset, byteLength, lane = -1, slot = -1, release, trim) {
|
|
99
|
+
if (mint !== MINT) {
|
|
100
|
+
throw new TypeError("KnittingSharedBuffer is not constructible; allocate one from " +
|
|
101
|
+
"createKnittingAllocator() or adopt a descriptor");
|
|
102
|
+
}
|
|
103
|
+
this.#buffer = buffer;
|
|
104
|
+
this.#byteOffset = byteOffset;
|
|
105
|
+
this.#byteLength = byteLength;
|
|
106
|
+
this.#lane = lane;
|
|
107
|
+
this.#slot = slot;
|
|
108
|
+
this.#release = release;
|
|
109
|
+
this.#trim = trim;
|
|
110
|
+
}
|
|
111
|
+
get byteLength() {
|
|
112
|
+
return this.#byteLength;
|
|
113
|
+
}
|
|
114
|
+
get byteOffset() {
|
|
115
|
+
return this.#byteOffset;
|
|
116
|
+
}
|
|
117
|
+
/** Owning lane, and the region identity within it (-1 for a standalone SAB). */
|
|
118
|
+
get lane() {
|
|
119
|
+
return this.#lane;
|
|
120
|
+
}
|
|
121
|
+
get slot() {
|
|
122
|
+
return this.#slot;
|
|
123
|
+
}
|
|
124
|
+
/** True once this handle is spent, whether by `release()` or by a move. */
|
|
125
|
+
get released() {
|
|
126
|
+
return this.#released;
|
|
127
|
+
}
|
|
128
|
+
/** True when ownership was handed to a consumer by `allocator.moveTo()`. */
|
|
129
|
+
get moved() {
|
|
130
|
+
return this.#moved;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The standalone SharedArrayBuffer behind an overflow region, or undefined
|
|
134
|
+
* for a pooled one.
|
|
135
|
+
*
|
|
136
|
+
* Pooled regions deliberately have no way to reach their backing store: it
|
|
137
|
+
* is the whole arena, and handing it out would expose every other live
|
|
138
|
+
* region's bytes. An overflow region owns its buffer outright, so it is the
|
|
139
|
+
* only one that can travel as a buffer rather than as a descriptor.
|
|
140
|
+
*/
|
|
141
|
+
static standaloneBufferOf(region) {
|
|
142
|
+
return region.#slot === -1 ? region.#buffer : undefined;
|
|
143
|
+
}
|
|
144
|
+
#assertLive() {
|
|
145
|
+
if (this.#moved) {
|
|
146
|
+
throw new Error("KnittingSharedBuffer: this region was moved to a consumer; it is " +
|
|
147
|
+
"the consumer's to read and to release. Fill it before moveTo(), " +
|
|
148
|
+
"or copy() the bytes you need to keep");
|
|
149
|
+
}
|
|
150
|
+
if (this.#released) {
|
|
151
|
+
throw new Error("KnittingSharedBuffer: this region was released; mint views before " +
|
|
152
|
+
"release, or copy() the bytes you need to keep");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Surrender the identity without freeing it: after this the handle is inert
|
|
157
|
+
* and the consumer that adopted the descriptor is the sole releaser.
|
|
158
|
+
*
|
|
159
|
+
* A no-op on a spent handle, so a double `moveTo()` cannot hand the same
|
|
160
|
+
* identity to two consumers -- the second call throws before reaching here.
|
|
161
|
+
*/
|
|
162
|
+
static [DISOWN](region) {
|
|
163
|
+
if (region.#released)
|
|
164
|
+
return;
|
|
165
|
+
region.#released = true;
|
|
166
|
+
region.#moved = true;
|
|
167
|
+
region.#u8 = undefined;
|
|
168
|
+
region.#views = undefined;
|
|
169
|
+
const release = region.#release;
|
|
170
|
+
region.#release = undefined;
|
|
171
|
+
region.#trim = undefined;
|
|
172
|
+
release?.(false);
|
|
173
|
+
}
|
|
174
|
+
/** The byte view. Memoized: repeated calls do not construct. */
|
|
175
|
+
u8() {
|
|
176
|
+
this.#assertLive();
|
|
177
|
+
const cached = this.#u8;
|
|
178
|
+
if (cached !== undefined)
|
|
179
|
+
return cached;
|
|
180
|
+
const view = new Uint8Array(this.#buffer, this.#byteOffset, this.#byteLength);
|
|
181
|
+
this.#u8 = view;
|
|
182
|
+
mintedViews.set(view, this);
|
|
183
|
+
return view;
|
|
184
|
+
}
|
|
185
|
+
/** A typed view over the whole region. Memoized per constructor. */
|
|
186
|
+
view(Ctor) {
|
|
187
|
+
this.#assertLive();
|
|
188
|
+
if (Ctor === Uint8Array)
|
|
189
|
+
return this.u8();
|
|
190
|
+
const views = this.#views ??= new Map();
|
|
191
|
+
const cached = views.get(Ctor);
|
|
192
|
+
if (cached !== undefined)
|
|
193
|
+
return cached;
|
|
194
|
+
const perElement = Ctor.BYTES_PER_ELEMENT ?? 1;
|
|
195
|
+
if (this.#byteLength % perElement !== 0) {
|
|
196
|
+
throw new RangeError(`region of ${this.#byteLength} bytes does not divide into ` +
|
|
197
|
+
`${perElement}-byte elements`);
|
|
198
|
+
}
|
|
199
|
+
const view = new Ctor(this.#buffer, this.#byteOffset, this.#byteLength / perElement);
|
|
200
|
+
views.set(Ctor, view);
|
|
201
|
+
mintedViews.set(view, this);
|
|
202
|
+
return view;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Give back the tail of a region that was reserved larger than needed, and
|
|
206
|
+
* report the region as `byteLength` bytes from here on.
|
|
207
|
+
*
|
|
208
|
+
* The point is streamed input of unknown length: an HTTP body with no
|
|
209
|
+
* `Content-Length` cannot be sized up front, so reserve an upper bound,
|
|
210
|
+
* write into it, then commit what actually arrived. Views minted before the
|
|
211
|
+
* commit are dropped, because their length is now wrong.
|
|
212
|
+
*
|
|
213
|
+
* Shrink only. Growing would mean relocating the bytes, which is the copy
|
|
214
|
+
* this whole path exists to avoid.
|
|
215
|
+
*/
|
|
216
|
+
commit(byteLength) {
|
|
217
|
+
this.#assertLive();
|
|
218
|
+
if (byteLength < 0 || byteLength > this.#byteLength) {
|
|
219
|
+
throw new RangeError(`commit(${byteLength}) must be between 0 and the reserved ` +
|
|
220
|
+
`${this.#byteLength} bytes`);
|
|
221
|
+
}
|
|
222
|
+
if (byteLength === this.#byteLength)
|
|
223
|
+
return this;
|
|
224
|
+
this.#byteLength = byteLength;
|
|
225
|
+
this.#u8 = undefined;
|
|
226
|
+
this.#views = undefined;
|
|
227
|
+
this.#trim?.(byteLength);
|
|
228
|
+
return this;
|
|
229
|
+
}
|
|
230
|
+
/** An independently owned copy, valid after this region is released. */
|
|
231
|
+
copy() {
|
|
232
|
+
this.#assertLive();
|
|
233
|
+
return new Uint8Array(this.u8());
|
|
234
|
+
}
|
|
235
|
+
release() {
|
|
236
|
+
if (this.#released)
|
|
237
|
+
return;
|
|
238
|
+
this.#released = true;
|
|
239
|
+
this.#u8 = undefined;
|
|
240
|
+
this.#views = undefined;
|
|
241
|
+
const release = this.#release;
|
|
242
|
+
this.#release = undefined;
|
|
243
|
+
this.#trim = undefined;
|
|
244
|
+
release?.(true);
|
|
245
|
+
}
|
|
246
|
+
[Symbol.dispose]() {
|
|
247
|
+
this.release();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Detection: given an arbitrary value on its way into a task payload, decide
|
|
252
|
+
* whether it is backed by a pooled region -- in which case the codec ships a
|
|
253
|
+
* descriptor instead of the bytes.
|
|
254
|
+
*
|
|
255
|
+
* Three strategies, cheapest first. `detectRegion` runs them in order.
|
|
256
|
+
*/
|
|
257
|
+
/** Views minted by a region, so a bare view traces back to its owner. */
|
|
258
|
+
const mintedViews = new WeakMap();
|
|
259
|
+
/** Arena SAB -> the pool that owns it, for views nobody minted. */
|
|
260
|
+
const arenaOwners = new WeakMap();
|
|
261
|
+
/** A region handed over directly. One `instanceof`. */
|
|
262
|
+
export const detectByInstance = (value) => value instanceof KnittingSharedBuffer ? value : undefined;
|
|
263
|
+
/**
|
|
264
|
+
* A view this pool minted. One WeakMap hit, and it never touches `.buffer` --
|
|
265
|
+
* which matters because reading `.buffer` on a heap-backed typed array can
|
|
266
|
+
* materialize the ArrayBuffer wrapper on first access.
|
|
267
|
+
*/
|
|
268
|
+
export const detectByMintedView = (value) => ArrayBuffer.isView(value) ? mintedViews.get(value) : undefined;
|
|
269
|
+
/**
|
|
270
|
+
* Any view that lands inside a pool arena, including a `subarray` of a minted
|
|
271
|
+
* view. Costs a `.buffer` read, a WeakMap hit, and a binary search over the
|
|
272
|
+
* live extent table.
|
|
273
|
+
*/
|
|
274
|
+
export const detectByArena = (value) => {
|
|
275
|
+
if (!ArrayBuffer.isView(value))
|
|
276
|
+
return undefined;
|
|
277
|
+
const owner = arenaOwners.get(value.buffer);
|
|
278
|
+
if (owner === undefined)
|
|
279
|
+
return undefined;
|
|
280
|
+
const slot = owner.slotContaining(value.byteOffset, value.byteLength);
|
|
281
|
+
if (slot === -1)
|
|
282
|
+
return undefined;
|
|
283
|
+
return {
|
|
284
|
+
codec: KNITTING_BUFFER_CODEC,
|
|
285
|
+
kind: "region",
|
|
286
|
+
lane: owner.lane,
|
|
287
|
+
slot,
|
|
288
|
+
byteOffset: value.byteOffset,
|
|
289
|
+
byteLength: value.byteLength,
|
|
290
|
+
};
|
|
291
|
+
};
|
|
292
|
+
/**
|
|
293
|
+
* The full check the codec would run on a payload value. Returns a descriptor
|
|
294
|
+
* to ship by reference, or undefined to serialize the value normally.
|
|
295
|
+
*
|
|
296
|
+
* Detection only: like `describe()`, it does not transfer ownership. Whatever
|
|
297
|
+
* ships the descriptor owes the consumer one of the two safe pairings
|
|
298
|
+
* documented on `describe()`.
|
|
299
|
+
*/
|
|
300
|
+
export const detectRegion = (value) => {
|
|
301
|
+
const region = value instanceof KnittingSharedBuffer
|
|
302
|
+
? value
|
|
303
|
+
: ArrayBuffer.isView(value)
|
|
304
|
+
? mintedViews.get(value)
|
|
305
|
+
: undefined;
|
|
306
|
+
if (region !== undefined) {
|
|
307
|
+
return region.slot === -1
|
|
308
|
+
? {
|
|
309
|
+
codec: KNITTING_BUFFER_CODEC,
|
|
310
|
+
kind: "buffer",
|
|
311
|
+
lane: region.lane,
|
|
312
|
+
byteLength: region.byteLength,
|
|
313
|
+
buffer: KnittingSharedBuffer.standaloneBufferOf(region),
|
|
314
|
+
}
|
|
315
|
+
: {
|
|
316
|
+
codec: KNITTING_BUFFER_CODEC,
|
|
317
|
+
kind: "region",
|
|
318
|
+
lane: region.lane,
|
|
319
|
+
slot: region.slot,
|
|
320
|
+
byteOffset: region.byteOffset,
|
|
321
|
+
byteLength: region.byteLength,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
return detectByArena(value);
|
|
325
|
+
};
|
|
326
|
+
const finalizers = new FinalizationRegistry((hold) => {
|
|
327
|
+
if (!hold.live)
|
|
328
|
+
return;
|
|
329
|
+
hold.live = false;
|
|
330
|
+
hold.free(hold.slot);
|
|
331
|
+
});
|
|
332
|
+
/**
|
|
333
|
+
* Bump window, and therefore the pool's memory high-water.
|
|
334
|
+
*
|
|
335
|
+
* Deliberately small. A wide window makes *allocation* cheaper -- the bump
|
|
336
|
+
* pointer runs longer before it has to reconcile and reclaim holes -- but that
|
|
337
|
+
* only counts the bookkeeping, never the bytes. Once real traffic writes them
|
|
338
|
+
* the ranking inverts: a wide window sprays consecutive regions across cold
|
|
339
|
+
* memory, and the cache misses cost more than the reconcile it avoided.
|
|
340
|
+
* Locality wins, so the default is a small multiple of a typical live set
|
|
341
|
+
* rather than the whole arena.
|
|
342
|
+
*
|
|
343
|
+
* It is a default, not a recommendation: size it to `payload x in-flight`.
|
|
344
|
+
* Two concurrent 1 MiB payloads do not fit here, and a reservation the window
|
|
345
|
+
* cannot satisfy takes the overflow path rather than waiting. `stats()`
|
|
346
|
+
* reports `overflows` precisely so this is visible instead of mysterious.
|
|
347
|
+
*/
|
|
348
|
+
export const DEFAULT_ARENA_BYTE_LENGTH = 2 * 1024 * 1024;
|
|
349
|
+
export const createKnittingAllocator = ({ lane = 0, slots = 128, arenaByteLength = DEFAULT_ARENA_BYTE_LENGTH, gcBackstop = true, backstopWatermark = 0.5, } = {}) => {
|
|
350
|
+
const lockSAB = new SharedArrayBuffer(LOCK_SECTOR_BYTE_LENGTH);
|
|
351
|
+
const arena = new SharedArrayBuffer(arenaByteLength);
|
|
352
|
+
const regions = createLazyRegionRegistry({
|
|
353
|
+
slots,
|
|
354
|
+
mode: "lazy",
|
|
355
|
+
arenaByteLength,
|
|
356
|
+
lockSector: lockSAB,
|
|
357
|
+
});
|
|
358
|
+
arenaOwners.set(arena, {
|
|
359
|
+
lane,
|
|
360
|
+
slotContaining: regions.slotContaining,
|
|
361
|
+
});
|
|
362
|
+
let overflows = 0;
|
|
363
|
+
let pooled = 0;
|
|
364
|
+
let live = 0;
|
|
365
|
+
let registered = 0;
|
|
366
|
+
const watermark = (slots * backstopWatermark) | 0;
|
|
367
|
+
const alloc = (byteLength) => {
|
|
368
|
+
const slot = regions.allocRegion(byteLength);
|
|
369
|
+
// Identity or arena exhausted: hand back a standalone SAB instead of
|
|
370
|
+
// evicting somebody's live borrow. Costs an allocation, never a stall.
|
|
371
|
+
if (slot === -1) {
|
|
372
|
+
overflows++;
|
|
373
|
+
return new KnittingSharedBuffer(MINT, new SharedArrayBuffer(byteLength), 0, byteLength, lane, -1);
|
|
374
|
+
}
|
|
375
|
+
pooled++;
|
|
376
|
+
live++;
|
|
377
|
+
const byteOffset = regions.regionStart(slot);
|
|
378
|
+
const trim = (committed) => {
|
|
379
|
+
regions.trimRegion(slot, committed);
|
|
380
|
+
};
|
|
381
|
+
// Both paths back to the pool -- an explicit release and a collected
|
|
382
|
+
// handle -- must run this. Freeing the identity without decrementing
|
|
383
|
+
// `live` latched `gcBackstop: "pressure"` on permanently once the
|
|
384
|
+
// watermark was crossed, and left `stats().live` counting handles the
|
|
385
|
+
// collector had already reclaimed.
|
|
386
|
+
const releaseSlot = (freed) => {
|
|
387
|
+
live--;
|
|
388
|
+
regions.free(freed);
|
|
389
|
+
};
|
|
390
|
+
if (gcBackstop === false || (gcBackstop === "pressure" && live < watermark)) {
|
|
391
|
+
let held = true;
|
|
392
|
+
return new KnittingSharedBuffer(MINT, arena, byteOffset, byteLength, lane, slot, (free) => {
|
|
393
|
+
if (!held)
|
|
394
|
+
return;
|
|
395
|
+
held = false;
|
|
396
|
+
if (free)
|
|
397
|
+
releaseSlot(slot);
|
|
398
|
+
else
|
|
399
|
+
live--;
|
|
400
|
+
}, trim);
|
|
401
|
+
}
|
|
402
|
+
const hold = { free: releaseSlot, slot, live: true };
|
|
403
|
+
const region = new KnittingSharedBuffer(MINT, arena, byteOffset, byteLength, lane, slot, (free) => {
|
|
404
|
+
if (!hold.live)
|
|
405
|
+
return;
|
|
406
|
+
hold.live = false;
|
|
407
|
+
finalizers.unregister(hold);
|
|
408
|
+
if (free)
|
|
409
|
+
releaseSlot(slot);
|
|
410
|
+
// A moved identity is still occupied -- by the consumer, who now owns
|
|
411
|
+
// the only release. It leaves this pool's handle count either way.
|
|
412
|
+
else
|
|
413
|
+
live--;
|
|
414
|
+
}, trim);
|
|
415
|
+
finalizers.register(region, hold, hold);
|
|
416
|
+
registered++;
|
|
417
|
+
return region;
|
|
418
|
+
};
|
|
419
|
+
/**
|
|
420
|
+
* Reserve up to `maxByteLength` for input whose real size is not known yet
|
|
421
|
+
* -- a chunked HTTP body, a stream. Fill it, then `region.commit(actual)` to
|
|
422
|
+
* hand the unused tail back.
|
|
423
|
+
*
|
|
424
|
+
* The bound must fit the bump window, and comfortably: a reservation the
|
|
425
|
+
* window cannot satisfy is not an error, it takes the overflow path and
|
|
426
|
+
* allocates a fresh SharedArrayBuffer, which is the most expensive thing
|
|
427
|
+
* this pool can do -- orders of magnitude past a pooled allocation, not a
|
|
428
|
+
* few percent. Size `arenaByteLength` to several concurrent reservations,
|
|
429
|
+
* and check `stats().overflows` if throughput looks wrong.
|
|
430
|
+
*/
|
|
431
|
+
const allocUpTo = (maxByteLength) => alloc(maxByteLength);
|
|
432
|
+
/**
|
|
433
|
+
* The wire form of a region, with no effect on ownership.
|
|
434
|
+
*
|
|
435
|
+
* Sending this to a consumer that adopts it normally leaves two independent
|
|
436
|
+
* releasers on one identity -- this handle (and its collector backstop) plus
|
|
437
|
+
* the consumer's -- which is the one way to defeat the toggle's ABA argument
|
|
438
|
+
* and hand a live region's bytes out twice. Two safe pairings:
|
|
439
|
+
*
|
|
440
|
+
* - `moveTo()` with a plain `adopt()`: the consumer owns and releases.
|
|
441
|
+
* - `describe()` with `adopt(d, { borrow: true })`: this pool keeps
|
|
442
|
+
* ownership, the consumer gets a handle that cannot release, and the
|
|
443
|
+
* region must outlive the call it is lent to.
|
|
444
|
+
*/
|
|
445
|
+
const describe = (region) => {
|
|
446
|
+
// `lane` is stamped from this pool, so a foreign region would name an
|
|
447
|
+
// identity in an arena the consumer resolves against the wrong pool.
|
|
448
|
+
if (region.lane !== lane) {
|
|
449
|
+
throw new Error(`region belongs to lane ${region.lane}, not lane ${lane}`);
|
|
450
|
+
}
|
|
451
|
+
if (region.slot === -1) {
|
|
452
|
+
return {
|
|
453
|
+
codec: KNITTING_BUFFER_CODEC,
|
|
454
|
+
kind: "buffer",
|
|
455
|
+
lane,
|
|
456
|
+
byteLength: region.byteLength,
|
|
457
|
+
// A SharedArrayBuffer only survives as a whole payload: knitting
|
|
458
|
+
// encodes a plain object with JSON.stringify, which renders any
|
|
459
|
+
// buffer nested inside it as `{}`. So this descriptor is for a
|
|
460
|
+
// same-isolate handoff; to send an overflow region to another
|
|
461
|
+
// thread, send `KnittingSharedBuffer.standaloneBufferOf(region)` as
|
|
462
|
+
// the payload itself and `adopt` that. `adopt` rejects a descriptor
|
|
463
|
+
// whose buffer did not survive rather than handing back a region over
|
|
464
|
+
// the wrong memory.
|
|
465
|
+
buffer: KnittingSharedBuffer.standaloneBufferOf(region),
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
return {
|
|
469
|
+
codec: KNITTING_BUFFER_CODEC,
|
|
470
|
+
kind: "region",
|
|
471
|
+
lane,
|
|
472
|
+
slot: region.slot,
|
|
473
|
+
byteOffset: region.byteOffset,
|
|
474
|
+
byteLength: region.byteLength,
|
|
475
|
+
};
|
|
476
|
+
};
|
|
477
|
+
/**
|
|
478
|
+
* Hand a region to a consumer: returns the descriptor to send and leaves
|
|
479
|
+
* this handle inert, so the consumer that adopts it is the sole releaser.
|
|
480
|
+
*
|
|
481
|
+
* This is the only safe way to send a region. The handle is spent on
|
|
482
|
+
* return -- `u8()`, `commit()` and a second `moveTo()` all throw -- so fill
|
|
483
|
+
* the region before moving it. Views minted earlier still alias the bytes;
|
|
484
|
+
* that is the same limitation `release()` has, and the same answer applies
|
|
485
|
+
* (`copy()` before moving if you need to keep reading).
|
|
486
|
+
*
|
|
487
|
+
* The identity is not freed here. It stays occupied until the consumer
|
|
488
|
+
* releases it and this pool's next `reconcile()` observes the toggle.
|
|
489
|
+
*/
|
|
490
|
+
const moveTo = (region) => {
|
|
491
|
+
if (region.released) {
|
|
492
|
+
throw new Error(region.moved
|
|
493
|
+
? "region was already moved to a consumer"
|
|
494
|
+
: "region was released and cannot be moved");
|
|
495
|
+
}
|
|
496
|
+
const descriptor = describe(region);
|
|
497
|
+
KnittingSharedBuffer[DISOWN](region);
|
|
498
|
+
return descriptor;
|
|
499
|
+
};
|
|
500
|
+
/**
|
|
501
|
+
* Read a request body into one disposable handle.
|
|
502
|
+
*
|
|
503
|
+
* Small bodies use a pooled descriptor; large or unknown bodies use a moved
|
|
504
|
+
* `BufferReference`. Send `body.wire`, read `body.u8()`, and dispose when done.
|
|
505
|
+
*
|
|
506
|
+
* Sends hold the body until settlement, so early disposal cannot recycle bytes
|
|
507
|
+
* still in use. For manual ownership, use `readBodyOrRefer()` directly.
|
|
508
|
+
*/
|
|
509
|
+
const allocOrRefer = async (request, options) => {
|
|
510
|
+
const payload = await readBodyOrRefer(request, { alloc, arenaByteLength }, options);
|
|
511
|
+
if (!(payload instanceof KnittingSharedBuffer)) {
|
|
512
|
+
// Hold moved references until the handle is disposed, not just the first
|
|
513
|
+
// call settles.
|
|
514
|
+
const hold = takePayloadTransportHold(payload);
|
|
515
|
+
const release = hold ?? (() => payload.release());
|
|
516
|
+
return {
|
|
517
|
+
wire: payload,
|
|
518
|
+
byteLength: payload.byteLength,
|
|
519
|
+
u8: () => payload.toUint8Array(),
|
|
520
|
+
release,
|
|
521
|
+
[Symbol.dispose]: release,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
const wire = wireFor(payload);
|
|
525
|
+
let inFlight = 0;
|
|
526
|
+
let disposed = false;
|
|
527
|
+
const settle = () => {
|
|
528
|
+
if (disposed && inFlight === 0)
|
|
529
|
+
payload.release();
|
|
530
|
+
};
|
|
531
|
+
// Only pooled regions return an identity to the registry.
|
|
532
|
+
if (!isTransportedBuffer(wire)) {
|
|
533
|
+
wire[PayloadTransportFinalizer] = () => {
|
|
534
|
+
inFlight++;
|
|
535
|
+
let done = false;
|
|
536
|
+
return () => {
|
|
537
|
+
if (done)
|
|
538
|
+
return;
|
|
539
|
+
done = true;
|
|
540
|
+
inFlight--;
|
|
541
|
+
settle();
|
|
542
|
+
};
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
const release = () => {
|
|
546
|
+
disposed = true;
|
|
547
|
+
settle();
|
|
548
|
+
};
|
|
549
|
+
return {
|
|
550
|
+
wire,
|
|
551
|
+
byteLength: payload.byteLength,
|
|
552
|
+
u8: () => payload.u8(),
|
|
553
|
+
release,
|
|
554
|
+
[Symbol.dispose]: release,
|
|
555
|
+
};
|
|
556
|
+
};
|
|
557
|
+
/**
|
|
558
|
+
* How a region travels: a descriptor when it is pooled, its own buffer when
|
|
559
|
+
* it overflowed the arena.
|
|
560
|
+
*
|
|
561
|
+
* An overflow region owns a standalone SharedArrayBuffer, which knitting
|
|
562
|
+
* preserves as a whole payload but renders as `{}` if it is nested inside
|
|
563
|
+
* one -- so it cannot ride in a descriptor. It is sized to the body that was
|
|
564
|
+
* *declared*; a body that arrived shorter was committed down, and the extra
|
|
565
|
+
* bytes are not ours to hand out, so that case is copied to an exact buffer.
|
|
566
|
+
*/
|
|
567
|
+
const wireFor = (region) => {
|
|
568
|
+
if (region.slot !== -1)
|
|
569
|
+
return describe(region);
|
|
570
|
+
const buffer = KnittingSharedBuffer.standaloneBufferOf(region);
|
|
571
|
+
if (buffer.byteLength === region.byteLength)
|
|
572
|
+
return buffer;
|
|
573
|
+
const exact = new SharedArrayBuffer(region.byteLength);
|
|
574
|
+
new Uint8Array(exact).set(region.u8());
|
|
575
|
+
return exact;
|
|
576
|
+
};
|
|
577
|
+
return {
|
|
578
|
+
lane,
|
|
579
|
+
/** Bump window this pool was built with; the ceiling on a pooled region. */
|
|
580
|
+
arenaByteLength,
|
|
581
|
+
alloc,
|
|
582
|
+
allocUpTo,
|
|
583
|
+
allocOrRefer,
|
|
584
|
+
describe,
|
|
585
|
+
moveTo,
|
|
586
|
+
reconcile: regions.reconcile,
|
|
587
|
+
// `regions.free` is deliberately not exposed: it XORs an identity with no
|
|
588
|
+
// check that the caller owns it, which would let a stray call make a live
|
|
589
|
+
// identity look released and hand the same bytes out twice.
|
|
590
|
+
stats: () => ({ ...regions.stats(), pooled, overflows, registered, live }),
|
|
591
|
+
resetCounters: () => {
|
|
592
|
+
pooled = 0;
|
|
593
|
+
overflows = 0;
|
|
594
|
+
registered = 0;
|
|
595
|
+
},
|
|
596
|
+
/** What a consumer thread needs to attach: SABs, not pointers. */
|
|
597
|
+
transport: () => ({ lane, lockSAB, arena, slots, arenaByteLength }),
|
|
598
|
+
};
|
|
599
|
+
};
|
|
600
|
+
/**
|
|
601
|
+
* The consumer side of another thread's pool. It never allocates; it
|
|
602
|
+
* materializes regions over the producer's arena and releases the producer's
|
|
603
|
+
* identity with one XOR into the shared word.
|
|
604
|
+
*/
|
|
605
|
+
export const attachKnittingAllocator = ({ lane, lockSAB, arena, slots }) => {
|
|
606
|
+
const words = slots >>> 5;
|
|
607
|
+
const workerBits = new Int32Array(lockSAB, PAYLOAD_LOCK_WORKER_BITS_OFFSET_BYTES, words);
|
|
608
|
+
// Indexed, not masked: `slots` need only be a multiple of 32, so folding
|
|
609
|
+
// with `slots - 1` aliased identities onto each other whenever it was not a
|
|
610
|
+
// power of two. `adopt` range-checks every descriptor before this runs.
|
|
611
|
+
const free = (slot) => {
|
|
612
|
+
if (!Number.isSafeInteger(slot) || slot < 0 || slot >= slots) {
|
|
613
|
+
throw new RangeError(`region identity ${slot} outside 0..${slots - 1}`);
|
|
614
|
+
}
|
|
615
|
+
Atomics.xor(workerBits, slot >>> 5, (1 << (slot & 31)) | 0);
|
|
616
|
+
};
|
|
617
|
+
/**
|
|
618
|
+
* Materialize a region over the producer's arena.
|
|
619
|
+
*
|
|
620
|
+
* `borrow: true` returns a handle with no release at all: `release()` is a
|
|
621
|
+
* no-op and no collector backstop is registered. That is the shape for a
|
|
622
|
+
* region lent for the duration of a call, where the producer stayed the
|
|
623
|
+
* owner -- the identity is a single shared bit, so a consumer that releases
|
|
624
|
+
* a borrowed region XORs it back to "in use" and strands it, or worse frees
|
|
625
|
+
* a region the producer has since recycled. Enforcing it here is cheaper
|
|
626
|
+
* than enforcing it by comment at every call site.
|
|
627
|
+
*/
|
|
628
|
+
const adopt = (descriptor, { gcBackstop = true, borrow = false } = {}) => {
|
|
629
|
+
// An overflow region's buffer travels on its own rather than nested in a
|
|
630
|
+
// descriptor; see `describe`. It owns no identity, so there is nothing to
|
|
631
|
+
// check it against and nothing to release.
|
|
632
|
+
if (isTransportedBuffer(descriptor)) {
|
|
633
|
+
return new KnittingSharedBuffer(MINT, descriptor, 0, descriptor.byteLength, lane, -1);
|
|
634
|
+
}
|
|
635
|
+
if (descriptor.kind === "buffer") {
|
|
636
|
+
const { buffer, byteLength } = descriptor;
|
|
637
|
+
if (!isTransportedBuffer(buffer)) {
|
|
638
|
+
throw new TypeError("descriptor of kind 'buffer' carries no buffer. A " +
|
|
639
|
+
"SharedArrayBuffer nested inside a payload object does not " +
|
|
640
|
+
"survive encoding; send it as the payload itself and adopt it " +
|
|
641
|
+
"directly.");
|
|
642
|
+
}
|
|
643
|
+
if (!Number.isSafeInteger(byteLength) || byteLength < 0 ||
|
|
644
|
+
byteLength > buffer.byteLength) {
|
|
645
|
+
throw new RangeError(`descriptor claims ${byteLength} bytes of a ` +
|
|
646
|
+
`${buffer.byteLength}-byte buffer`);
|
|
647
|
+
}
|
|
648
|
+
return new KnittingSharedBuffer(MINT, buffer, 0, byteLength, descriptor.lane, -1);
|
|
649
|
+
}
|
|
650
|
+
if (descriptor.lane !== lane) {
|
|
651
|
+
throw new Error(`descriptor for lane ${descriptor.lane} adopted on lane ${lane}`);
|
|
652
|
+
}
|
|
653
|
+
const { slot, byteOffset, byteLength } = descriptor;
|
|
654
|
+
// A descriptor is a plain object, so treat it as input. These checks stop
|
|
655
|
+
// a malformed one from aliasing outside the arena or releasing an identity
|
|
656
|
+
// that does not exist. They cannot stop a descriptor that points at
|
|
657
|
+
// another *live* region: the extent table is owned by the producing
|
|
658
|
+
// thread and is not in shared memory, so a peer has no way to check it.
|
|
659
|
+
// That is why adopting is part of the transport surface and not the app
|
|
660
|
+
// one -- descriptors are produced by the runtime, never by user code.
|
|
661
|
+
if (!Number.isSafeInteger(slot) || slot < 0 || slot >= slots) {
|
|
662
|
+
throw new RangeError(`descriptor names identity ${slot}, outside 0..${slots - 1}`);
|
|
663
|
+
}
|
|
664
|
+
if (!Number.isSafeInteger(byteOffset) || !Number.isSafeInteger(byteLength) ||
|
|
665
|
+
byteOffset < 0 || byteLength < 0 ||
|
|
666
|
+
byteOffset + byteLength > arena.byteLength) {
|
|
667
|
+
throw new RangeError(`descriptor spans [${byteOffset}, ${byteOffset + byteLength}) ` +
|
|
668
|
+
`outside the ${arena.byteLength}-byte arena`);
|
|
669
|
+
}
|
|
670
|
+
if (borrow) {
|
|
671
|
+
return new KnittingSharedBuffer(MINT, arena, byteOffset, byteLength, lane, slot);
|
|
672
|
+
}
|
|
673
|
+
if (!gcBackstop) {
|
|
674
|
+
let held = true;
|
|
675
|
+
return new KnittingSharedBuffer(MINT, arena, byteOffset, byteLength, lane, slot, () => {
|
|
676
|
+
if (!held)
|
|
677
|
+
return;
|
|
678
|
+
held = false;
|
|
679
|
+
free(slot);
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
const hold = { free, slot, live: true };
|
|
683
|
+
const region = new KnittingSharedBuffer(MINT, arena, byteOffset, byteLength, lane, slot, () => {
|
|
684
|
+
if (!hold.live)
|
|
685
|
+
return;
|
|
686
|
+
hold.live = false;
|
|
687
|
+
finalizers.unregister(hold);
|
|
688
|
+
free(slot);
|
|
689
|
+
});
|
|
690
|
+
finalizers.register(region, hold, hold);
|
|
691
|
+
return region;
|
|
692
|
+
};
|
|
693
|
+
// `free` stays internal here for the same reason it does on the allocator.
|
|
694
|
+
return { lane, adopt };
|
|
695
|
+
};
|