bun-shared-memory 0.1.5 → 0.1.7
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/CHANGELOG.md +100 -0
- package/README.md +41 -28
- package/package.json +1 -1
- package/src/growable-buffer.ts +441 -45
- package/src/memory.ts +306 -166
- package/src/refcount.ts +19 -4
- package/src/shared.ts +5 -5
- package/src/worker.ts +60 -3
package/src/growable-buffer.ts
CHANGED
|
@@ -1,67 +1,268 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared memory budget shared across multiple GrowableBuffers.
|
|
3
3
|
*
|
|
4
|
-
* Tracks the
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* stays within the budget.
|
|
4
|
+
* Tracks the combined physical size of DataTable, StringTable, and ArrayTable.
|
|
5
|
+
* The counter is shared across workers so each growth reserves against the
|
|
6
|
+
* same limit.
|
|
8
7
|
*/
|
|
9
8
|
export class SharedBudget {
|
|
10
|
-
private
|
|
9
|
+
private readonly used: BigInt64Array;
|
|
11
10
|
|
|
12
|
-
constructor(
|
|
11
|
+
constructor(
|
|
12
|
+
private readonly max: number,
|
|
13
|
+
buffer = new SharedArrayBuffer(BigInt64Array.BYTES_PER_ELEMENT),
|
|
14
|
+
) {
|
|
15
|
+
this.used = new BigInt64Array(buffer);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get buffer(): SharedArrayBuffer {
|
|
19
|
+
return this.used.buffer as SharedArrayBuffer;
|
|
20
|
+
}
|
|
13
21
|
|
|
14
22
|
/** Record a buffer's initial physical size. */
|
|
15
23
|
add(size: number): void {
|
|
16
|
-
this.
|
|
17
|
-
|
|
24
|
+
if (!this.tryReserve(size)) {
|
|
25
|
+
const total = Atomics.load(this.used, 0) + BigInt(size);
|
|
18
26
|
throw new RangeError(
|
|
19
|
-
`Shared memory budget exceeded: initial buffers total ${
|
|
27
|
+
`Shared memory budget exceeded: initial buffers total ${total} bytes but the budget is ${this.max}`,
|
|
20
28
|
);
|
|
21
29
|
}
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
/**
|
|
25
|
-
*
|
|
26
|
-
* updates the total if the new total stays within the budget, false
|
|
27
|
-
* otherwise (the caller must not grow).
|
|
33
|
+
* Atomically reserve physical memory shared by all tables and workers.
|
|
28
34
|
*/
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
tryReserve(size: number): boolean {
|
|
36
|
+
if (size <= 0) return true;
|
|
37
|
+
const increment = BigInt(size);
|
|
38
|
+
for (;;) {
|
|
39
|
+
const current = Atomics.load(this.used, 0);
|
|
40
|
+
if (current + increment > BigInt(this.max)) return false;
|
|
41
|
+
if (
|
|
42
|
+
Atomics.compareExchange(this.used, 0, current, current + increment) ===
|
|
43
|
+
current
|
|
44
|
+
) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Release memory reserved for a growth that failed or a reclaimed chunk.
|
|
52
|
+
*/
|
|
53
|
+
release(size: number): void {
|
|
54
|
+
if (size <= 0) return;
|
|
55
|
+
const decrement = BigInt(size);
|
|
56
|
+
for (;;) {
|
|
57
|
+
const current = Atomics.load(this.used, 0);
|
|
58
|
+
const next = current > decrement ? current - decrement : 0n;
|
|
59
|
+
if (Atomics.compareExchange(this.used, 0, current, next) === current) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
34
63
|
}
|
|
35
64
|
}
|
|
36
65
|
|
|
37
66
|
/**
|
|
38
|
-
* Growable
|
|
67
|
+
* Growable shared memory backed by one or more SharedArrayBuffers. Chunked
|
|
68
|
+
* tables split storage across SABs; the Index Table uses one growable SAB.
|
|
69
|
+
* Offsets are logical and continuous across chunks.
|
|
39
70
|
*
|
|
40
|
-
*
|
|
41
|
-
* `
|
|
42
|
-
*
|
|
43
|
-
*
|
|
71
|
+
* In chunked mode, all chunks are pre-allocated with `maxByteLength =
|
|
72
|
+
* chunkSize` but only 4 bytes of actual storage. A chunk is activated (grown
|
|
73
|
+
* to its full size) on first access. Because `SharedArrayBuffer.grow()` is
|
|
74
|
+
* visible across workers, activation needs no postMessage broadcast.
|
|
44
75
|
*/
|
|
76
|
+
interface Chunk {
|
|
77
|
+
buffer: SharedArrayBuffer;
|
|
78
|
+
view: DataView;
|
|
79
|
+
/** Whether the chunk has been grown to its full size. */
|
|
80
|
+
active: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Round `n` up to the next power of two (chunk sizes must be powers of two). */
|
|
84
|
+
export function nextPowerOfTwo(n: number): number {
|
|
85
|
+
if (n <= 0) return 1;
|
|
86
|
+
let p = 1;
|
|
87
|
+
while (p < n) p <<= 1;
|
|
88
|
+
return p;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function largestPowerOfTwo(n: number): number {
|
|
92
|
+
if (n <= 1) return 1;
|
|
93
|
+
return 2 ** Math.floor(Math.log2(n));
|
|
94
|
+
}
|
|
95
|
+
|
|
45
96
|
export class GrowableBuffer {
|
|
46
|
-
|
|
47
|
-
private view: DataView;
|
|
97
|
+
private chunks: Chunk[] = [];
|
|
48
98
|
private readonly maxByteLength: number;
|
|
99
|
+
private readonly singleBuffer: boolean;
|
|
100
|
+
/** Fixed size of every chunk (the minimum growth step). */
|
|
101
|
+
readonly chunkSize: number;
|
|
102
|
+
/** log2(chunkSize), used for O(1) offset resolution via bit shifts. */
|
|
103
|
+
private readonly chunkShift: number;
|
|
49
104
|
private readonly budget?: SharedBudget;
|
|
50
105
|
private used = 0;
|
|
106
|
+
/** Smallest chunk index that is not yet active (see `activate`). */
|
|
107
|
+
private nextChunkToActivate = 0;
|
|
51
108
|
|
|
52
109
|
constructor(
|
|
53
|
-
initialSizeOrBuffer: number | SharedArrayBuffer,
|
|
110
|
+
initialSizeOrBuffer: number | SharedArrayBuffer | SharedArrayBuffer[],
|
|
54
111
|
maxByteLength: number,
|
|
55
112
|
budget?: SharedBudget,
|
|
113
|
+
chunkSize = 32 * 1024 * 1024, // 32 MiB per chunk
|
|
114
|
+
singleBuffer = false,
|
|
115
|
+
chargeInitial = true,
|
|
56
116
|
) {
|
|
57
|
-
this.
|
|
58
|
-
|
|
59
|
-
|
|
117
|
+
this.singleBuffer = singleBuffer;
|
|
118
|
+
this.budget = budget;
|
|
119
|
+
if (singleBuffer && typeof initialSizeOrBuffer !== "number") {
|
|
120
|
+
const buffer = Array.isArray(initialSizeOrBuffer)
|
|
121
|
+
? initialSizeOrBuffer[0]
|
|
60
122
|
: initialSizeOrBuffer;
|
|
61
|
-
|
|
123
|
+
if (
|
|
124
|
+
buffer === undefined ||
|
|
125
|
+
(Array.isArray(initialSizeOrBuffer) && initialSizeOrBuffer.length !== 1)
|
|
126
|
+
) {
|
|
127
|
+
throw new RangeError(
|
|
128
|
+
"A single-buffer GrowableBuffer requires exactly one SharedArrayBuffer",
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
this.maxByteLength = Math.min(
|
|
132
|
+
buffer.maxByteLength ?? buffer.byteLength,
|
|
133
|
+
maxByteLength,
|
|
134
|
+
);
|
|
135
|
+
this.chunkSize = this.maxByteLength;
|
|
136
|
+
this.chunkShift = 0;
|
|
137
|
+
this.chunks.push({
|
|
138
|
+
buffer,
|
|
139
|
+
view: new DataView(buffer),
|
|
140
|
+
active: buffer.byteLength > 4,
|
|
141
|
+
});
|
|
142
|
+
if (chargeInitial) this.budget?.add(buffer.byteLength);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
62
146
|
this.maxByteLength = maxByteLength;
|
|
63
|
-
|
|
64
|
-
|
|
147
|
+
if (typeof initialSizeOrBuffer === "number") {
|
|
148
|
+
if (singleBuffer) {
|
|
149
|
+
const buffer = new SharedArrayBuffer(4, {
|
|
150
|
+
maxByteLength,
|
|
151
|
+
});
|
|
152
|
+
this.chunkSize = maxByteLength;
|
|
153
|
+
this.chunkShift = 0;
|
|
154
|
+
this.chunks.push({
|
|
155
|
+
buffer,
|
|
156
|
+
view: new DataView(buffer),
|
|
157
|
+
active: false,
|
|
158
|
+
});
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
this.chunkSize = Math.min(
|
|
162
|
+
nextPowerOfTwo(chunkSize),
|
|
163
|
+
largestPowerOfTwo(maxByteLength),
|
|
164
|
+
);
|
|
165
|
+
this.chunkShift = Math.log2(this.chunkSize);
|
|
166
|
+
// Pre-allocate all chunks (4 bytes each, growable to chunkSize).
|
|
167
|
+
const chunkCount = Math.max(1, Math.ceil(maxByteLength / this.chunkSize));
|
|
168
|
+
for (let i = 0; i < chunkCount; i++) {
|
|
169
|
+
const buffer = new SharedArrayBuffer(4, {
|
|
170
|
+
maxByteLength: this.chunkSize,
|
|
171
|
+
});
|
|
172
|
+
this.chunks.push({
|
|
173
|
+
buffer,
|
|
174
|
+
view: new DataView(buffer),
|
|
175
|
+
active: false,
|
|
176
|
+
});
|
|
177
|
+
if (chargeInitial) this.budget?.add(buffer.byteLength);
|
|
178
|
+
}
|
|
179
|
+
} else if (Array.isArray(initialSizeOrBuffer)) {
|
|
180
|
+
// Reconstruct from a list of chunks. Use the buffers directly (they are
|
|
181
|
+
// already shared via postMessage) so writes are visible across workers.
|
|
182
|
+
// All chunks are pre-allocated at 4 bytes with maxByteLength = chunkSize;
|
|
183
|
+
// the chunk size is derived from each buffer's maxByteLength (which is
|
|
184
|
+
// preserved across postMessage), not its current byteLength.
|
|
185
|
+
this.chunkSize = initialSizeOrBuffer[0]?.maxByteLength ?? chunkSize;
|
|
186
|
+
this.chunkShift = Math.log2(this.chunkSize);
|
|
187
|
+
for (const sab of initialSizeOrBuffer) {
|
|
188
|
+
this.chunks.push({
|
|
189
|
+
buffer: sab,
|
|
190
|
+
view: new DataView(sab),
|
|
191
|
+
active: sab.byteLength === this.chunkSize,
|
|
192
|
+
});
|
|
193
|
+
if (chargeInitial) this.budget?.add(sab.byteLength);
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
// Reconstruct from a single existing buffer. Use it directly so writes
|
|
197
|
+
// are shared. Its size defines the chunk size (prefer maxByteLength so
|
|
198
|
+
// a pre-allocated 4-byte chunk is interpreted at its full size).
|
|
199
|
+
this.chunkSize =
|
|
200
|
+
initialSizeOrBuffer.maxByteLength ?? initialSizeOrBuffer.byteLength;
|
|
201
|
+
this.chunkShift = Math.log2(this.chunkSize);
|
|
202
|
+
this.chunks.push({
|
|
203
|
+
buffer: initialSizeOrBuffer,
|
|
204
|
+
view: new DataView(initialSizeOrBuffer),
|
|
205
|
+
active: initialSizeOrBuffer.byteLength === this.chunkSize,
|
|
206
|
+
});
|
|
207
|
+
if (chargeInitial) this.budget?.add(initialSizeOrBuffer.byteLength);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Activate the chunk at `index` by growing it to its full size. Because
|
|
213
|
+
* `SharedArrayBuffer.grow()` is visible across all workers, no broadcast is
|
|
214
|
+
* needed. No-op if the chunk is already active.
|
|
215
|
+
*/
|
|
216
|
+
private activate(index: number): void {
|
|
217
|
+
const chunk = this.chunks[index];
|
|
218
|
+
if (chunk === undefined || chunk.active) return;
|
|
219
|
+
if (this.singleBuffer) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const oldSize = chunk.buffer.byteLength;
|
|
223
|
+
if (oldSize >= this.chunkSize) {
|
|
224
|
+
chunk.view = new DataView(chunk.buffer);
|
|
225
|
+
chunk.active = true;
|
|
226
|
+
this.advanceNextChunkToActivate(index);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const growth = this.chunkSize - oldSize;
|
|
230
|
+
if (this.budget && !this.budget.tryReserve(growth)) {
|
|
231
|
+
throw new RangeError(
|
|
232
|
+
`Shared memory budget exceeded: need ${growth} additional bytes`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
chunk.buffer.grow(this.chunkSize);
|
|
237
|
+
} catch (error) {
|
|
238
|
+
if (chunk.buffer.byteLength >= this.chunkSize) {
|
|
239
|
+
this.budget?.release(growth);
|
|
240
|
+
chunk.view = new DataView(chunk.buffer);
|
|
241
|
+
chunk.active = true;
|
|
242
|
+
this.advanceNextChunkToActivate(index);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
this.budget?.release(growth);
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
chunk.view = new DataView(chunk.buffer);
|
|
249
|
+
chunk.active = true;
|
|
250
|
+
this.advanceNextChunkToActivate(index);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Advance `nextChunkToActivate` past `index` while the following chunks are
|
|
255
|
+
* already active, so `ensureCapacity` never re-scans active chunks.
|
|
256
|
+
*/
|
|
257
|
+
private advanceNextChunkToActivate(index: number): void {
|
|
258
|
+
if (index < this.nextChunkToActivate) return;
|
|
259
|
+
this.nextChunkToActivate = index + 1;
|
|
260
|
+
while (
|
|
261
|
+
this.nextChunkToActivate < this.chunks.length &&
|
|
262
|
+
this.chunks[this.nextChunkToActivate]!.active
|
|
263
|
+
) {
|
|
264
|
+
this.nextChunkToActivate++;
|
|
265
|
+
}
|
|
65
266
|
}
|
|
66
267
|
|
|
67
268
|
/** Number of bytes written so far (used length, not buffer capacity). */
|
|
@@ -69,24 +270,105 @@ export class GrowableBuffer {
|
|
|
69
270
|
return this.used;
|
|
70
271
|
}
|
|
71
272
|
|
|
273
|
+
/** Total logical capacity across all chunks (active or not). */
|
|
274
|
+
get capacity(): number {
|
|
275
|
+
return this.singleBuffer
|
|
276
|
+
? this.maxByteLength
|
|
277
|
+
: this.chunks.length * this.chunkSize;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** The list of SharedArrayBuffers backing this buffer (for export). */
|
|
281
|
+
get buffers(): SharedArrayBuffer[] {
|
|
282
|
+
return this.chunks.map((c) => c.buffer);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Number of chunks that have been grown from their initial 4-byte size. */
|
|
286
|
+
get activeChunkCount(): number {
|
|
287
|
+
return this.chunks.reduce((n, c) => n + (c.active ? 1 : 0), 0);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Replace the chunk list with `newBuffers` (received from the main thread
|
|
292
|
+
* after a defragmentation broadcast). Each buffer is either a full-size
|
|
293
|
+
* active chunk or a fresh 4-byte inactive chunk. Used by workers to stay in
|
|
294
|
+
* sync with the main thread's chunk replacement.
|
|
295
|
+
*/
|
|
296
|
+
applyChunks(newBuffers: SharedArrayBuffer[]): void {
|
|
297
|
+
if (this.singleBuffer && newBuffers.length !== 1) {
|
|
298
|
+
throw new RangeError(
|
|
299
|
+
"A single-buffer GrowableBuffer requires exactly one SharedArrayBuffer",
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
this.chunks = newBuffers.map((sab) => ({
|
|
303
|
+
buffer: sab,
|
|
304
|
+
view: new DataView(sab),
|
|
305
|
+
active: this.singleBuffer
|
|
306
|
+
? sab.byteLength > 4
|
|
307
|
+
: sab.byteLength === this.chunkSize,
|
|
308
|
+
}));
|
|
309
|
+
this.nextChunkToActivate = this.firstInactiveChunk();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Resolve a logical offset into (chunk, offset within that chunk). All
|
|
314
|
+
* chunks are the same size, so this is O(1). Activates the chunk if it is
|
|
315
|
+
* not yet active.
|
|
316
|
+
*/
|
|
317
|
+
private resolve(offset: number): { chunk: Chunk; off: number } {
|
|
318
|
+
if (this.singleBuffer) {
|
|
319
|
+
return { chunk: this.chunks[0]!, off: offset };
|
|
320
|
+
}
|
|
321
|
+
const index = offset >>> this.chunkShift;
|
|
322
|
+
const chunk = this.chunks[index];
|
|
323
|
+
if (chunk === undefined) {
|
|
324
|
+
throw new RangeError(`Offset ${offset} is beyond the buffer capacity`);
|
|
325
|
+
}
|
|
326
|
+
this.activate(index);
|
|
327
|
+
return { chunk, off: offset & (this.chunkSize - 1) };
|
|
328
|
+
}
|
|
329
|
+
|
|
72
330
|
/** Ensure the buffer has room for at least `needed` bytes. */
|
|
73
331
|
ensureCapacity(needed: number): void {
|
|
74
|
-
if (needed <= this.buffer.byteLength) return;
|
|
75
332
|
if (needed > this.maxByteLength) {
|
|
76
333
|
throw new RangeError(
|
|
77
334
|
`Shared buffer capacity exceeded: need ${needed} bytes but max is ${this.maxByteLength}`,
|
|
78
335
|
);
|
|
79
336
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
337
|
+
if (this.singleBuffer) {
|
|
338
|
+
const chunk = this.chunks[0]!;
|
|
339
|
+
const oldSize = chunk.buffer.byteLength;
|
|
340
|
+
if (needed <= oldSize) return;
|
|
341
|
+
const target = Math.min(
|
|
342
|
+
this.maxByteLength,
|
|
343
|
+
Math.max(needed, oldSize * 2),
|
|
86
344
|
);
|
|
345
|
+
let growth = target - oldSize;
|
|
346
|
+
if (this.budget && !this.budget.tryReserve(growth)) {
|
|
347
|
+
if (target === needed || !this.budget.tryReserve(needed - oldSize)) {
|
|
348
|
+
throw new RangeError(
|
|
349
|
+
`Shared memory budget exceeded: need ${needed - oldSize} additional bytes`,
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
growth = needed - oldSize;
|
|
353
|
+
}
|
|
354
|
+
try {
|
|
355
|
+
chunk.buffer.grow(oldSize + growth);
|
|
356
|
+
} catch (error) {
|
|
357
|
+
this.budget?.release(growth);
|
|
358
|
+
throw error;
|
|
359
|
+
}
|
|
360
|
+
chunk.view = new DataView(chunk.buffer);
|
|
361
|
+
chunk.active = true;
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
// All chunks are pre-allocated; just activate the ones needed to cover
|
|
365
|
+
// `needed`. grow() is visible across workers, so no broadcast is required.
|
|
366
|
+
// Start from `nextChunkToActivate` so already-active chunks are not
|
|
367
|
+
// re-scanned on every write (keeps the write hot path O(1) amortized).
|
|
368
|
+
const neededChunks = Math.ceil(needed / this.chunkSize);
|
|
369
|
+
for (let i = this.nextChunkToActivate; i < neededChunks; i++) {
|
|
370
|
+
this.activate(i);
|
|
87
371
|
}
|
|
88
|
-
this.buffer.grow(newSize);
|
|
89
|
-
this.view = new DataView(this.buffer);
|
|
90
372
|
}
|
|
91
373
|
|
|
92
374
|
/** Mark `n` bytes as used, after writing them at the current offset. */
|
|
@@ -96,15 +378,129 @@ export class GrowableBuffer {
|
|
|
96
378
|
|
|
97
379
|
/**
|
|
98
380
|
* Reset the used length to `n` bytes. Used by defragmentation after
|
|
99
|
-
* compacting live data to the front of the buffer.
|
|
100
|
-
*
|
|
101
|
-
*
|
|
381
|
+
* compacting live data to the front of the buffer. Chunks beyond the
|
|
382
|
+
* compacted region are marked inactive; their storage is reclaimed by
|
|
383
|
+
* `releaseInactiveChunks()`.
|
|
102
384
|
*/
|
|
103
385
|
reset(n: number): void {
|
|
104
386
|
this.used = n;
|
|
387
|
+
if (this.singleBuffer) return;
|
|
388
|
+
const neededChunks = Math.ceil(n / this.chunkSize);
|
|
389
|
+
for (let i = 0; i < this.chunks.length; i++) {
|
|
390
|
+
const chunk = this.chunks[i]!;
|
|
391
|
+
chunk.active =
|
|
392
|
+
i < neededChunks && chunk.buffer.byteLength >= this.chunkSize;
|
|
393
|
+
}
|
|
394
|
+
this.nextChunkToActivate = this.firstInactiveChunk();
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Replace every inactive chunk with a fresh 4-byte SharedArrayBuffer
|
|
399
|
+
* (growable to `chunkSize`), releasing the old full-size buffer so its
|
|
400
|
+
* memory can be reclaimed. Returns the new chunk list; the caller must
|
|
401
|
+
* broadcast it to workers so they see the replacement.
|
|
402
|
+
*
|
|
403
|
+
* Because a SharedArrayBuffer cannot be shrunk, reclaiming memory requires
|
|
404
|
+
* allocating a new buffer. This is only called after defragmentation (rare),
|
|
405
|
+
* so the resulting broadcast is acceptable.
|
|
406
|
+
*/
|
|
407
|
+
releaseInactiveChunks(): SharedArrayBuffer[] {
|
|
408
|
+
if (this.singleBuffer) return this.chunks.map((c) => c.buffer);
|
|
409
|
+
for (let i = 0; i < this.chunks.length; i++) {
|
|
410
|
+
const chunk = this.chunks[i]!;
|
|
411
|
+
if (chunk.active) continue;
|
|
412
|
+
// Release the old full-size buffer from the budget and replace it with
|
|
413
|
+
// a fresh 4-byte chunk.
|
|
414
|
+
this.budget?.release(Math.max(0, chunk.buffer.byteLength - 4));
|
|
415
|
+
const buffer = new SharedArrayBuffer(4, {
|
|
416
|
+
maxByteLength: this.chunkSize,
|
|
417
|
+
});
|
|
418
|
+
this.chunks[i] = { buffer, view: new DataView(buffer), active: false };
|
|
419
|
+
}
|
|
420
|
+
this.nextChunkToActivate = this.firstInactiveChunk();
|
|
421
|
+
return this.chunks.map((c) => c.buffer);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** Index of the first chunk that is not active, or `chunks.length`. */
|
|
425
|
+
private firstInactiveChunk(): number {
|
|
426
|
+
for (let i = 0; i < this.chunks.length; i++) {
|
|
427
|
+
if (!this.chunks[i]!.active) return i;
|
|
428
|
+
}
|
|
429
|
+
return this.chunks.length;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// -------------------------------------------------------------------------
|
|
433
|
+
// Typed accessors (resolve offset -> chunk)
|
|
434
|
+
// -------------------------------------------------------------------------
|
|
435
|
+
|
|
436
|
+
getInt32(offset: number): number {
|
|
437
|
+
if (this.singleBuffer) return this.chunks[0]!.view.getInt32(offset, true);
|
|
438
|
+
const index = offset >>> this.chunkShift;
|
|
439
|
+
this.activate(index);
|
|
440
|
+
return this.chunks[index]!.view.getInt32(
|
|
441
|
+
offset & (this.chunkSize - 1),
|
|
442
|
+
true,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
setInt32(offset: number, value: number): void {
|
|
447
|
+
if (this.singleBuffer) {
|
|
448
|
+
this.chunks[0]!.view.setInt32(offset, value, true);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const index = offset >>> this.chunkShift;
|
|
452
|
+
this.activate(index);
|
|
453
|
+
this.chunks[index]!.view.setInt32(
|
|
454
|
+
offset & (this.chunkSize - 1),
|
|
455
|
+
value,
|
|
456
|
+
true,
|
|
457
|
+
);
|
|
105
458
|
}
|
|
106
459
|
|
|
107
|
-
|
|
108
|
-
return this.view;
|
|
460
|
+
getUint8(offset: number): number {
|
|
461
|
+
if (this.singleBuffer) return this.chunks[0]!.view.getUint8(offset);
|
|
462
|
+
const index = offset >>> this.chunkShift;
|
|
463
|
+
this.activate(index);
|
|
464
|
+
return this.chunks[index]!.view.getUint8(offset & (this.chunkSize - 1));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
setUint8(offset: number, value: number): void {
|
|
468
|
+
if (this.singleBuffer) {
|
|
469
|
+
this.chunks[0]!.view.setUint8(offset, value);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const index = offset >>> this.chunkShift;
|
|
473
|
+
this.activate(index);
|
|
474
|
+
this.chunks[index]!.view.setUint8(offset & (this.chunkSize - 1), value);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** Read `length` bytes starting at `offset` into a fresh Uint8Array. */
|
|
478
|
+
readBytes(offset: number, length: number): Uint8Array {
|
|
479
|
+
const out = new Uint8Array(length);
|
|
480
|
+
let pos = offset;
|
|
481
|
+
let outPos = 0;
|
|
482
|
+
while (outPos < length) {
|
|
483
|
+
const { chunk, off } = this.resolve(pos);
|
|
484
|
+
const n = Math.min(length - outPos, this.chunkSize - off);
|
|
485
|
+
out.set(new Uint8Array(chunk.buffer, off, n), outPos);
|
|
486
|
+
outPos += n;
|
|
487
|
+
pos += n;
|
|
488
|
+
}
|
|
489
|
+
return out;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** Write `bytes` starting at `offset`. */
|
|
493
|
+
writeBytes(offset: number, bytes: Uint8Array): void {
|
|
494
|
+
let pos = offset;
|
|
495
|
+
let srcPos = 0;
|
|
496
|
+
while (srcPos < bytes.byteLength) {
|
|
497
|
+
const { chunk, off } = this.resolve(pos);
|
|
498
|
+
const n = Math.min(bytes.byteLength - srcPos, this.chunkSize - off);
|
|
499
|
+
new Uint8Array(chunk.buffer, off, n).set(
|
|
500
|
+
bytes.subarray(srcPos, srcPos + n),
|
|
501
|
+
);
|
|
502
|
+
srcPos += n;
|
|
503
|
+
pos += n;
|
|
504
|
+
}
|
|
109
505
|
}
|
|
110
506
|
}
|