bun-shared-memory 0.1.5 → 0.1.6
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 +47 -0
- package/package.json +1 -1
- package/src/growable-buffer.ts +197 -29
- package/src/memory.ts +159 -160
- package/src/refcount.ts +3 -3
- package/src/shared.ts +5 -5
- package/src/worker.ts +13 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,9 +5,47 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.1.6] - 2026-11-02
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Support shared memory larger than 4 GiB. The single-`SharedArrayBuffer`
|
|
13
|
+
per-buffer architecture is replaced with a chunked `GrowableBuffer`: each
|
|
14
|
+
buffer is backed by multiple equal-size chunks (32 MiB default, power of
|
|
15
|
+
two), so a buffer can exceed the 4 GiB single-SAB limit. Offsets resolve
|
|
16
|
+
O(1) via bit shifts (`offset >> chunkShift`).
|
|
17
|
+
- `SharedMemoryOptions.chunkSize` to configure the chunk size (the minimum
|
|
18
|
+
growth step).
|
|
19
|
+
- Chunks are pre-allocated at construction (4 bytes each, with
|
|
20
|
+
`maxByteLength = chunkSize`) and activated on first access via
|
|
21
|
+
`SharedArrayBuffer.grow()`. Because `grow()` is visible across all workers
|
|
22
|
+
sharing a buffer, a worker can grow a chunk and every other worker sees it
|
|
23
|
+
immediately — no postMessage broadcast is needed for chunk activation.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- `exportBuffers()`/`fromBuffers()` now pass chunk lists instead of a single
|
|
28
|
+
buffer; `fromBuffers` reuses the shared `SharedArrayBuffer`s directly (no
|
|
29
|
+
copy) so writes stay visible across workers. The chunk size is derived from
|
|
30
|
+
each buffer's `maxByteLength` (preserved across `postMessage`), not its
|
|
31
|
+
current 4-byte length.
|
|
32
|
+
- Remove the 4 GiB cap on the per-buffer maximum in the `SharedMemory`
|
|
33
|
+
constructor (the chunked `GrowableBuffer` already bypasses the single-SAB
|
|
34
|
+
limit).
|
|
35
|
+
|
|
36
|
+
### Performance
|
|
37
|
+
|
|
38
|
+
- After defragmentation, unused chunks are marked inactive:
|
|
39
|
+
`GrowableBuffer.reset()` deactivates chunks beyond the compacted region so
|
|
40
|
+
the GC can reclaim their storage.
|
|
41
|
+
- The chunked accessor adds an indirect `chunks[index].view` lookup on the
|
|
42
|
+
write hot path (the price of >4 GiB support): writes ~-15%, array writes
|
|
43
|
+
~-20%. Reads are unchanged (lock-free cache path).
|
|
44
|
+
|
|
8
45
|
## [0.1.5] - 2026-11-02
|
|
9
46
|
|
|
10
47
|
### Changed
|
|
48
|
+
|
|
11
49
|
- Replace the `Proxy` used for object `SharedRef`s with hard-baked
|
|
12
50
|
`Object.defineProperty` accessor getters/setters, so property reads skip the
|
|
13
51
|
Proxy trap on the hot path (~3× faster object property reads). Methods
|
|
@@ -24,6 +62,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
24
62
|
## [0.1.4] - 2026-11-01
|
|
25
63
|
|
|
26
64
|
### Changed
|
|
65
|
+
|
|
27
66
|
- Declare `"sideEffects": false` in `package.json` so bundlers can tree-shake
|
|
28
67
|
the library (the modules have no side effects on import).
|
|
29
68
|
- Remove the `typescript` peer dependency. The library ships as TypeScript
|
|
@@ -33,6 +72,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
33
72
|
## [0.1.3] - 2026-11-01
|
|
34
73
|
|
|
35
74
|
### Added
|
|
75
|
+
|
|
36
76
|
- Real-world benchmark (`bench/real-world/`) comparing shared memory against
|
|
37
77
|
live SQLite, MySQL, and Redis via an Elysia mini API.
|
|
38
78
|
- Redis as a fourth benchmark methodology (in Docker).
|
|
@@ -41,6 +81,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
41
81
|
- Real-world benchmark results and methodology documented in the README.
|
|
42
82
|
|
|
43
83
|
### Changed
|
|
84
|
+
|
|
44
85
|
- MySQL/Redis connections are established before the benchmark (not per
|
|
45
86
|
request) so connection setup is not measured as overhead.
|
|
46
87
|
- The benchmark always runs both TCP and Unix-socket transports; the socket
|
|
@@ -49,16 +90,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
49
90
|
## [0.1.2] - 2026-11-01
|
|
50
91
|
|
|
51
92
|
### Changed
|
|
93
|
+
|
|
52
94
|
- Unref the maintenance timer so the process can exit naturally.
|
|
53
95
|
- Document `init()` as the first call and how to configure memory via `init()`.
|
|
54
96
|
|
|
55
97
|
## [0.1.1] - 2026-11-01
|
|
56
98
|
|
|
57
99
|
### Added
|
|
100
|
+
|
|
58
101
|
- Repository metadata and npm publishing preparation.
|
|
59
102
|
- README with package name and install instructions.
|
|
60
103
|
|
|
61
104
|
### Changed
|
|
105
|
+
|
|
62
106
|
- Optimize the read hot path: direct cache on the proxy target.
|
|
63
107
|
- Optimize `Mutex`: skip notify when no worker is waiting.
|
|
64
108
|
- Start the refcount coordinator/tracker automatically in `init()`.
|
|
@@ -67,6 +111,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
67
111
|
## [0.1.0] - 2026-11-01
|
|
68
112
|
|
|
69
113
|
### Added
|
|
114
|
+
|
|
70
115
|
- Initial release: `Shared<T>` Proxy-based API over a 4-buffer shared memory
|
|
71
116
|
architecture (Index Table, DataTable, append-only StringTable, ArrayTable).
|
|
72
117
|
- Atomics-based synchronization (`Mutex`, `Notifier`).
|
|
@@ -75,6 +120,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
75
120
|
- O(1) in-place `push`/`pop` with capacity-based array growth.
|
|
76
121
|
- Benchmarks for all data types vs native.
|
|
77
122
|
|
|
123
|
+
[0.1.6]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.5...v0.1.6
|
|
124
|
+
[0.1.5]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.4...v0.1.5
|
|
78
125
|
[0.1.4]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.3...v0.1.4
|
|
79
126
|
[0.1.3]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.2...v0.1.3
|
|
80
127
|
[0.1.2]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.1...v0.1.2
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bun-shared-memory",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Share data between Bun workers/threads using SharedArrayBuffer (zero-copy, 4-buffer architecture, Atomics-based synchronization).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
package/src/growable-buffer.ts
CHANGED
|
@@ -35,33 +35,111 @@ export class SharedBudget {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* Growable
|
|
38
|
+
* Growable shared memory backed by one or more SharedArrayBuffers (chunks).
|
|
39
39
|
*
|
|
40
|
-
* SharedArrayBuffer
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
40
|
+
* A single SharedArrayBuffer can be at most 4 GiB, so a buffer that needs to
|
|
41
|
+
* grow beyond that is split into multiple chunks. Each chunk is its own
|
|
42
|
+
* SharedArrayBuffer (<= `chunkMax` bytes). Offsets are logical and continuous
|
|
43
|
+
* across chunks; access methods resolve an offset into (chunk, chunkOffset).
|
|
44
|
+
*
|
|
45
|
+
* All chunks are pre-allocated at construction with `maxByteLength = chunkSize`
|
|
46
|
+
* but only 4 bytes of actual storage. A chunk is "activated" (grown to its
|
|
47
|
+
* full `chunkSize`) on first access. Because `SharedArrayBuffer.grow()` is
|
|
48
|
+
* visible across all workers sharing the buffer, activation needs no
|
|
49
|
+
* postMessage broadcast.
|
|
44
50
|
*/
|
|
51
|
+
interface Chunk {
|
|
52
|
+
buffer: SharedArrayBuffer;
|
|
53
|
+
view: DataView;
|
|
54
|
+
/** Whether the chunk has been grown to its full size. */
|
|
55
|
+
active: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Round `n` up to the next power of two (chunk sizes must be powers of two). */
|
|
59
|
+
function nextPowerOfTwo(n: number): number {
|
|
60
|
+
if (n <= 0) return 1;
|
|
61
|
+
let p = 1;
|
|
62
|
+
while (p < n) p <<= 1;
|
|
63
|
+
return p;
|
|
64
|
+
}
|
|
65
|
+
|
|
45
66
|
export class GrowableBuffer {
|
|
46
|
-
|
|
47
|
-
private view: DataView;
|
|
67
|
+
private chunks: Chunk[] = [];
|
|
48
68
|
private readonly maxByteLength: number;
|
|
69
|
+
/** Fixed size of every chunk (the minimum growth step). */
|
|
70
|
+
readonly chunkSize: number;
|
|
71
|
+
/** log2(chunkSize), used for O(1) offset resolution via bit shifts. */
|
|
72
|
+
private readonly chunkShift: number;
|
|
49
73
|
private readonly budget?: SharedBudget;
|
|
50
74
|
private used = 0;
|
|
51
75
|
|
|
52
76
|
constructor(
|
|
53
|
-
initialSizeOrBuffer: number | SharedArrayBuffer,
|
|
77
|
+
initialSizeOrBuffer: number | SharedArrayBuffer | SharedArrayBuffer[],
|
|
54
78
|
maxByteLength: number,
|
|
55
79
|
budget?: SharedBudget,
|
|
80
|
+
chunkSize = 32 * 1024 * 1024, // 32 MiB per chunk
|
|
56
81
|
) {
|
|
57
|
-
this.buffer =
|
|
58
|
-
typeof initialSizeOrBuffer === "number"
|
|
59
|
-
? new SharedArrayBuffer(initialSizeOrBuffer, { maxByteLength })
|
|
60
|
-
: initialSizeOrBuffer;
|
|
61
|
-
this.view = new DataView(this.buffer);
|
|
62
82
|
this.maxByteLength = maxByteLength;
|
|
63
83
|
this.budget = budget;
|
|
64
|
-
|
|
84
|
+
if (typeof initialSizeOrBuffer === "number") {
|
|
85
|
+
this.chunkSize = Math.min(nextPowerOfTwo(chunkSize), maxByteLength);
|
|
86
|
+
this.chunkShift = Math.log2(this.chunkSize);
|
|
87
|
+
// Pre-allocate all chunks (4 bytes each, growable to chunkSize).
|
|
88
|
+
const chunkCount = Math.max(1, Math.ceil(maxByteLength / this.chunkSize));
|
|
89
|
+
for (let i = 0; i < chunkCount; i++) {
|
|
90
|
+
const buffer = new SharedArrayBuffer(4, {
|
|
91
|
+
maxByteLength: this.chunkSize,
|
|
92
|
+
});
|
|
93
|
+
this.chunks.push({
|
|
94
|
+
buffer,
|
|
95
|
+
view: new DataView(buffer),
|
|
96
|
+
active: false,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
} else if (Array.isArray(initialSizeOrBuffer)) {
|
|
100
|
+
// Reconstruct from a list of chunks. Use the buffers directly (they are
|
|
101
|
+
// already shared via postMessage) so writes are visible across workers.
|
|
102
|
+
// All chunks are pre-allocated at 4 bytes with maxByteLength = chunkSize;
|
|
103
|
+
// the chunk size is derived from each buffer's maxByteLength (which is
|
|
104
|
+
// preserved across postMessage), not its current byteLength.
|
|
105
|
+
this.chunkSize = initialSizeOrBuffer[0]?.maxByteLength ?? chunkSize;
|
|
106
|
+
this.chunkShift = Math.log2(this.chunkSize);
|
|
107
|
+
for (const sab of initialSizeOrBuffer) {
|
|
108
|
+
this.chunks.push({
|
|
109
|
+
buffer: sab,
|
|
110
|
+
view: new DataView(sab),
|
|
111
|
+
active: sab.byteLength === this.chunkSize,
|
|
112
|
+
});
|
|
113
|
+
this.budget?.add(sab.byteLength);
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
// Reconstruct from a single existing buffer. Use it directly so writes
|
|
117
|
+
// are shared. Its size defines the chunk size (prefer maxByteLength so
|
|
118
|
+
// a pre-allocated 4-byte chunk is interpreted at its full size).
|
|
119
|
+
this.chunkSize =
|
|
120
|
+
initialSizeOrBuffer.maxByteLength ?? initialSizeOrBuffer.byteLength;
|
|
121
|
+
this.chunkShift = Math.log2(this.chunkSize);
|
|
122
|
+
this.chunks.push({
|
|
123
|
+
buffer: initialSizeOrBuffer,
|
|
124
|
+
view: new DataView(initialSizeOrBuffer),
|
|
125
|
+
active: initialSizeOrBuffer.byteLength === this.chunkSize,
|
|
126
|
+
});
|
|
127
|
+
this.budget?.add(initialSizeOrBuffer.byteLength);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Activate the chunk at `index` by growing it to its full size. Because
|
|
133
|
+
* `SharedArrayBuffer.grow()` is visible across all workers, no broadcast is
|
|
134
|
+
* needed. No-op if the chunk is already active.
|
|
135
|
+
*/
|
|
136
|
+
private activate(index: number): void {
|
|
137
|
+
const chunk = this.chunks[index];
|
|
138
|
+
if (chunk === undefined || chunk.active) return;
|
|
139
|
+
chunk.buffer.grow(this.chunkSize);
|
|
140
|
+
chunk.view = new DataView(chunk.buffer);
|
|
141
|
+
chunk.active = true;
|
|
142
|
+
this.budget?.add(this.chunkSize);
|
|
65
143
|
}
|
|
66
144
|
|
|
67
145
|
/** Number of bytes written so far (used length, not buffer capacity). */
|
|
@@ -69,24 +147,49 @@ export class GrowableBuffer {
|
|
|
69
147
|
return this.used;
|
|
70
148
|
}
|
|
71
149
|
|
|
150
|
+
/** Total logical capacity across all chunks (active or not). */
|
|
151
|
+
get capacity(): number {
|
|
152
|
+
return this.chunks.length * this.chunkSize;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The list of SharedArrayBuffers backing this buffer (for export). */
|
|
156
|
+
get buffers(): SharedArrayBuffer[] {
|
|
157
|
+
return this.chunks.map((c) => c.buffer);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Number of chunks that have been activated (grown to full size). */
|
|
161
|
+
get activeChunkCount(): number {
|
|
162
|
+
return this.chunks.reduce((n, c) => n + (c.active ? 1 : 0), 0);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Resolve a logical offset into (chunk, offset within that chunk). All
|
|
167
|
+
* chunks are the same size, so this is O(1). Activates the chunk if it is
|
|
168
|
+
* not yet active.
|
|
169
|
+
*/
|
|
170
|
+
private resolve(offset: number): { chunk: Chunk; off: number } {
|
|
171
|
+
const index = offset >> this.chunkShift;
|
|
172
|
+
const chunk = this.chunks[index];
|
|
173
|
+
if (chunk === undefined) {
|
|
174
|
+
throw new RangeError(`Offset ${offset} is beyond the buffer capacity`);
|
|
175
|
+
}
|
|
176
|
+
this.activate(index);
|
|
177
|
+
return { chunk, off: offset & (this.chunkSize - 1) };
|
|
178
|
+
}
|
|
179
|
+
|
|
72
180
|
/** Ensure the buffer has room for at least `needed` bytes. */
|
|
73
181
|
ensureCapacity(needed: number): void {
|
|
74
|
-
if (needed <= this.buffer.byteLength) return;
|
|
75
182
|
if (needed > this.maxByteLength) {
|
|
76
183
|
throw new RangeError(
|
|
77
184
|
`Shared buffer capacity exceeded: need ${needed} bytes but max is ${this.maxByteLength}`,
|
|
78
185
|
);
|
|
79
186
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
`Shared memory budget exceeded: growing to ${newSize} bytes would exceed the total budget`,
|
|
86
|
-
);
|
|
187
|
+
// All chunks are pre-allocated; just activate the ones needed to cover
|
|
188
|
+
// `needed`. grow() is visible across workers, so no broadcast is required.
|
|
189
|
+
const neededChunks = Math.ceil(needed / this.chunkSize);
|
|
190
|
+
for (let i = 0; i < neededChunks; i++) {
|
|
191
|
+
this.activate(i);
|
|
87
192
|
}
|
|
88
|
-
this.buffer.grow(newSize);
|
|
89
|
-
this.view = new DataView(this.buffer);
|
|
90
193
|
}
|
|
91
194
|
|
|
92
195
|
/** Mark `n` bytes as used, after writing them at the current offset. */
|
|
@@ -96,15 +199,80 @@ export class GrowableBuffer {
|
|
|
96
199
|
|
|
97
200
|
/**
|
|
98
201
|
* Reset the used length to `n` bytes. Used by defragmentation after
|
|
99
|
-
* compacting live data to the front of the buffer.
|
|
100
|
-
*
|
|
101
|
-
*
|
|
202
|
+
* compacting live data to the front of the buffer. Chunks beyond the
|
|
203
|
+
* compacted region are marked inactive (their storage is released by the
|
|
204
|
+
* GC, which replaces them with fresh chunks).
|
|
102
205
|
*/
|
|
103
206
|
reset(n: number): void {
|
|
104
207
|
this.used = n;
|
|
208
|
+
const neededChunks = Math.ceil(n / this.chunkSize);
|
|
209
|
+
for (let i = neededChunks; i < this.chunks.length; i++) {
|
|
210
|
+
this.chunks[i]!.active = false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// -------------------------------------------------------------------------
|
|
215
|
+
// Typed accessors (resolve offset -> chunk)
|
|
216
|
+
// -------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
getInt32(offset: number): number {
|
|
219
|
+
const index = offset >> this.chunkShift;
|
|
220
|
+
this.activate(index);
|
|
221
|
+
return this.chunks[index]!.view.getInt32(
|
|
222
|
+
offset & (this.chunkSize - 1),
|
|
223
|
+
true,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
setInt32(offset: number, value: number): void {
|
|
228
|
+
const index = offset >> this.chunkShift;
|
|
229
|
+
this.activate(index);
|
|
230
|
+
this.chunks[index]!.view.setInt32(
|
|
231
|
+
offset & (this.chunkSize - 1),
|
|
232
|
+
value,
|
|
233
|
+
true,
|
|
234
|
+
);
|
|
105
235
|
}
|
|
106
236
|
|
|
107
|
-
|
|
108
|
-
|
|
237
|
+
getUint8(offset: number): number {
|
|
238
|
+
const index = offset >> this.chunkShift;
|
|
239
|
+
this.activate(index);
|
|
240
|
+
return this.chunks[index]!.view.getUint8(offset & (this.chunkSize - 1));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
setUint8(offset: number, value: number): void {
|
|
244
|
+
const index = offset >> this.chunkShift;
|
|
245
|
+
this.activate(index);
|
|
246
|
+
this.chunks[index]!.view.setUint8(offset & (this.chunkSize - 1), value);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Read `length` bytes starting at `offset` into a fresh Uint8Array. */
|
|
250
|
+
readBytes(offset: number, length: number): Uint8Array {
|
|
251
|
+
const out = new Uint8Array(length);
|
|
252
|
+
let pos = offset;
|
|
253
|
+
let outPos = 0;
|
|
254
|
+
while (outPos < length) {
|
|
255
|
+
const { chunk, off } = this.resolve(pos);
|
|
256
|
+
const n = Math.min(length - outPos, this.chunkSize - off);
|
|
257
|
+
out.set(new Uint8Array(chunk.buffer, off, n), outPos);
|
|
258
|
+
outPos += n;
|
|
259
|
+
pos += n;
|
|
260
|
+
}
|
|
261
|
+
return out;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Write `bytes` starting at `offset`. */
|
|
265
|
+
writeBytes(offset: number, bytes: Uint8Array): void {
|
|
266
|
+
let pos = offset;
|
|
267
|
+
let srcPos = 0;
|
|
268
|
+
while (srcPos < bytes.byteLength) {
|
|
269
|
+
const { chunk, off } = this.resolve(pos);
|
|
270
|
+
const n = Math.min(bytes.byteLength - srcPos, this.chunkSize - off);
|
|
271
|
+
new Uint8Array(chunk.buffer, off, n).set(
|
|
272
|
+
bytes.subarray(srcPos, srcPos + n),
|
|
273
|
+
);
|
|
274
|
+
srcPos += n;
|
|
275
|
+
pos += n;
|
|
276
|
+
}
|
|
109
277
|
}
|
|
110
278
|
}
|
package/src/memory.ts
CHANGED
|
@@ -175,19 +175,35 @@ export interface SharedMemoryOptions {
|
|
|
175
175
|
maxByteLength?: number;
|
|
176
176
|
/** Total memory budget shared across all buffers (see below). */
|
|
177
177
|
maxTotalBytes?: number;
|
|
178
|
+
/** Size of each growable chunk (the minimum growth step). Default 32 MiB. */
|
|
179
|
+
chunkSize?: number;
|
|
178
180
|
lock?: Mutex;
|
|
179
181
|
internCapacity?: number;
|
|
180
182
|
internMax?: number;
|
|
181
183
|
buffers?: {
|
|
182
|
-
indexTable?: SharedArrayBuffer;
|
|
183
|
-
dataTable?: SharedArrayBuffer;
|
|
184
|
-
stringTable?: SharedArrayBuffer;
|
|
185
|
-
arrayTable?: SharedArrayBuffer;
|
|
184
|
+
indexTable?: SharedArrayBuffer | SharedArrayBuffer[];
|
|
185
|
+
dataTable?: SharedArrayBuffer | SharedArrayBuffer[];
|
|
186
|
+
stringTable?: SharedArrayBuffer | SharedArrayBuffer[];
|
|
187
|
+
arrayTable?: SharedArrayBuffer | SharedArrayBuffer[];
|
|
186
188
|
layoutVersion?: SharedArrayBuffer;
|
|
187
189
|
refCount?: SharedArrayBuffer;
|
|
188
190
|
};
|
|
189
191
|
}
|
|
190
192
|
|
|
193
|
+
/** Build a GrowableBuffer from an optional single SAB, chunk list, or nothing. */
|
|
194
|
+
function makeBuffer(
|
|
195
|
+
buf: SharedArrayBuffer | SharedArrayBuffer[] | undefined,
|
|
196
|
+
initial: number,
|
|
197
|
+
maxByteLength: number,
|
|
198
|
+
budget?: SharedBudget,
|
|
199
|
+
chunkSize?: number,
|
|
200
|
+
): GrowableBuffer {
|
|
201
|
+
if (buf === undefined) {
|
|
202
|
+
return new GrowableBuffer(initial, maxByteLength, budget, chunkSize);
|
|
203
|
+
}
|
|
204
|
+
return new GrowableBuffer(buf, maxByteLength, budget, chunkSize);
|
|
205
|
+
}
|
|
206
|
+
|
|
191
207
|
/**
|
|
192
208
|
* Manages the four shared buffers and provides operations to store and read
|
|
193
209
|
* strings, arrays, and objects.
|
|
@@ -246,44 +262,48 @@ export class SharedMemory {
|
|
|
246
262
|
|
|
247
263
|
constructor(options?: SharedMemoryOptions) {
|
|
248
264
|
const initial = options?.initialSize ?? DEFAULT_INITIAL;
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
265
|
+
// `maxTotalBytes` is a *total* budget shared across all buffers: each
|
|
266
|
+
// buffer's own maxByteLength is capped at maxTotalBytes, and the sum of
|
|
267
|
+
// all buffers' physical sizes is additionally capped at maxTotalBytes.
|
|
268
|
+
// Without maxTotalBytes, each buffer grows up to its own maxByteLength.
|
|
269
|
+
// A single SharedArrayBuffer is limited to 4 GiB, but the chunked
|
|
270
|
+
// GrowableBuffer splits a buffer across multiple chunks, so a buffer can
|
|
271
|
+
// exceed 4 GiB.
|
|
254
272
|
const maxTotal = options?.maxTotalBytes;
|
|
255
|
-
const perBufferMax =
|
|
256
|
-
4 * 1024 * 1024 * 1024,
|
|
257
|
-
maxTotal ?? options?.maxByteLength ?? DEFAULT_MAX,
|
|
258
|
-
);
|
|
273
|
+
const perBufferMax = maxTotal ?? options?.maxByteLength ?? DEFAULT_MAX;
|
|
259
274
|
const budget =
|
|
260
275
|
maxTotal !== undefined ? new SharedBudget(maxTotal) : undefined;
|
|
276
|
+
const chunkSize = options?.chunkSize;
|
|
261
277
|
const buffers = options?.buffers;
|
|
262
|
-
this.indexTable =
|
|
263
|
-
buffers?.indexTable
|
|
264
|
-
|
|
278
|
+
this.indexTable = makeBuffer(
|
|
279
|
+
buffers?.indexTable,
|
|
280
|
+
initial,
|
|
265
281
|
perBufferMax,
|
|
266
282
|
budget,
|
|
283
|
+
chunkSize,
|
|
267
284
|
);
|
|
268
|
-
this.dataTable =
|
|
269
|
-
buffers?.dataTable
|
|
270
|
-
|
|
285
|
+
this.dataTable = makeBuffer(
|
|
286
|
+
buffers?.dataTable,
|
|
287
|
+
initial,
|
|
271
288
|
perBufferMax,
|
|
272
289
|
budget,
|
|
290
|
+
chunkSize,
|
|
273
291
|
);
|
|
274
|
-
this.stringTable =
|
|
275
|
-
buffers?.stringTable
|
|
276
|
-
|
|
292
|
+
this.stringTable = makeBuffer(
|
|
293
|
+
buffers?.stringTable,
|
|
294
|
+
initial,
|
|
277
295
|
perBufferMax,
|
|
278
296
|
budget,
|
|
297
|
+
chunkSize,
|
|
279
298
|
);
|
|
280
|
-
this.arrayTable =
|
|
281
|
-
buffers?.arrayTable
|
|
282
|
-
|
|
299
|
+
this.arrayTable = makeBuffer(
|
|
300
|
+
buffers?.arrayTable,
|
|
301
|
+
initial,
|
|
283
302
|
perBufferMax,
|
|
284
303
|
budget,
|
|
304
|
+
chunkSize,
|
|
285
305
|
);
|
|
286
|
-
this.versionView = new Int32Array(this.indexTable.
|
|
306
|
+
this.versionView = new Int32Array(this.indexTable.buffers[0]!);
|
|
287
307
|
this.layoutVersion = new Int32Array(
|
|
288
308
|
buffers?.layoutVersion ??
|
|
289
309
|
new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT),
|
|
@@ -313,21 +333,24 @@ export class SharedMemory {
|
|
|
313
333
|
/**
|
|
314
334
|
* Export all shared buffers (including the lock) so they can be sent to a
|
|
315
335
|
* worker via `postMessage` and reconstructed there with `fromBuffers()`.
|
|
336
|
+
*
|
|
337
|
+
* Each table is exported as a list of SharedArrayBuffers (chunks), since a
|
|
338
|
+
* single SharedArrayBuffer is limited to 4 GiB.
|
|
316
339
|
*/
|
|
317
340
|
exportBuffers(): {
|
|
318
|
-
indexTable: SharedArrayBuffer;
|
|
319
|
-
dataTable: SharedArrayBuffer;
|
|
320
|
-
stringTable: SharedArrayBuffer;
|
|
321
|
-
arrayTable: SharedArrayBuffer;
|
|
341
|
+
indexTable: SharedArrayBuffer[];
|
|
342
|
+
dataTable: SharedArrayBuffer[];
|
|
343
|
+
stringTable: SharedArrayBuffer[];
|
|
344
|
+
arrayTable: SharedArrayBuffer[];
|
|
322
345
|
layoutVersion: SharedArrayBuffer;
|
|
323
346
|
lock: SharedArrayBuffer;
|
|
324
347
|
refCount: SharedArrayBuffer | null;
|
|
325
348
|
} {
|
|
326
349
|
return {
|
|
327
|
-
indexTable: this.indexTable.
|
|
328
|
-
dataTable: this.dataTable.
|
|
329
|
-
stringTable: this.stringTable.
|
|
330
|
-
arrayTable: this.arrayTable.
|
|
350
|
+
indexTable: this.indexTable.buffers,
|
|
351
|
+
dataTable: this.dataTable.buffers,
|
|
352
|
+
stringTable: this.stringTable.buffers,
|
|
353
|
+
arrayTable: this.arrayTable.buffers,
|
|
331
354
|
layoutVersion: this.layoutVersion.buffer as SharedArrayBuffer,
|
|
332
355
|
lock: this.lock.buffer as SharedArrayBuffer,
|
|
333
356
|
refCount: this.refCountBuffer,
|
|
@@ -339,10 +362,10 @@ export class SharedMemory {
|
|
|
339
362
|
* `exportBuffers()` (e.g. received from the main thread in a worker).
|
|
340
363
|
*/
|
|
341
364
|
static fromBuffers(buffers: {
|
|
342
|
-
indexTable: SharedArrayBuffer;
|
|
343
|
-
dataTable: SharedArrayBuffer;
|
|
344
|
-
stringTable: SharedArrayBuffer;
|
|
345
|
-
arrayTable: SharedArrayBuffer;
|
|
365
|
+
indexTable: SharedArrayBuffer | SharedArrayBuffer[];
|
|
366
|
+
dataTable: SharedArrayBuffer | SharedArrayBuffer[];
|
|
367
|
+
stringTable: SharedArrayBuffer | SharedArrayBuffer[];
|
|
368
|
+
arrayTable: SharedArrayBuffer | SharedArrayBuffer[];
|
|
346
369
|
layoutVersion: SharedArrayBuffer;
|
|
347
370
|
lock: SharedArrayBuffer;
|
|
348
371
|
refCount?: SharedArrayBuffer | null;
|
|
@@ -393,10 +416,7 @@ export class SharedMemory {
|
|
|
393
416
|
const bytes = this.encoder.encode(value);
|
|
394
417
|
const offset = this.stringTable.byteLength;
|
|
395
418
|
this.stringTable.ensureCapacity(offset + bytes.byteLength);
|
|
396
|
-
|
|
397
|
-
for (let i = 0; i < bytes.byteLength; i++) {
|
|
398
|
-
view.setUint8(offset + i, bytes[i]!);
|
|
399
|
-
}
|
|
419
|
+
this.stringTable.writeBytes(offset, bytes);
|
|
400
420
|
this.stringTable.advance(bytes.byteLength);
|
|
401
421
|
const ref: StringRef = { offset, length: bytes.byteLength };
|
|
402
422
|
|
|
@@ -431,12 +451,7 @@ export class SharedMemory {
|
|
|
431
451
|
|
|
432
452
|
/** Read a string from the StringTable by reference. */
|
|
433
453
|
readString(ref: StringRef): string {
|
|
434
|
-
const
|
|
435
|
-
const bytes = new Uint8Array(
|
|
436
|
-
this.stringTable.buffer,
|
|
437
|
-
ref.offset,
|
|
438
|
-
ref.length,
|
|
439
|
-
);
|
|
454
|
+
const bytes = this.stringTable.readBytes(ref.offset, ref.length);
|
|
440
455
|
return this.decoder.decode(bytes);
|
|
441
456
|
}
|
|
442
457
|
|
|
@@ -458,11 +473,14 @@ export class SharedMemory {
|
|
|
458
473
|
const cap = Math.max(capacity ?? 0, count, 1);
|
|
459
474
|
const offset = this.arrayTable.byteLength;
|
|
460
475
|
this.arrayTable.ensureCapacity(offset + ARRAY_HEADER + cap * SLOT_SIZE);
|
|
461
|
-
const view = this.arrayTable.getView();
|
|
462
476
|
const objectId = this.nextObjectId++;
|
|
463
|
-
|
|
477
|
+
this.arrayTable.setInt32(offset, objectId);
|
|
464
478
|
for (let i = 0; i < count; i++) {
|
|
465
|
-
writeSlot(
|
|
479
|
+
writeSlot(
|
|
480
|
+
this.arrayTable,
|
|
481
|
+
offset + ARRAY_HEADER + i * SLOT_SIZE,
|
|
482
|
+
slots[i]!,
|
|
483
|
+
);
|
|
466
484
|
}
|
|
467
485
|
this.arrayTable.advance(ARRAY_HEADER + cap * SLOT_SIZE);
|
|
468
486
|
return { offset, count, capacity: cap };
|
|
@@ -470,10 +488,11 @@ export class SharedMemory {
|
|
|
470
488
|
|
|
471
489
|
/** Read an array of value slots from the ArrayTable by reference. */
|
|
472
490
|
readArray(ref: ArrayRef): ValueSlot[] {
|
|
473
|
-
const view = this.arrayTable.getView();
|
|
474
491
|
const slots: ValueSlot[] = [];
|
|
475
492
|
for (let i = 0; i < ref.count; i++) {
|
|
476
|
-
slots.push(
|
|
493
|
+
slots.push(
|
|
494
|
+
readSlot(this.arrayTable, ref.offset + ARRAY_HEADER + i * SLOT_SIZE),
|
|
495
|
+
);
|
|
477
496
|
}
|
|
478
497
|
return slots;
|
|
479
498
|
}
|
|
@@ -481,7 +500,7 @@ export class SharedMemory {
|
|
|
481
500
|
/** Update a single value slot of an array in the ArrayTable in place. */
|
|
482
501
|
updateArraySlot(ref: ArrayRef, index: number, slot: ValueSlot): void {
|
|
483
502
|
writeSlot(
|
|
484
|
-
this.arrayTable
|
|
503
|
+
this.arrayTable,
|
|
485
504
|
ref.offset + ARRAY_HEADER + index * SLOT_SIZE,
|
|
486
505
|
slot,
|
|
487
506
|
);
|
|
@@ -490,14 +509,14 @@ export class SharedMemory {
|
|
|
490
509
|
/** Read a single value slot of an array from the ArrayTable. */
|
|
491
510
|
readArraySlot(ref: ArrayRef, index: number): ValueSlot {
|
|
492
511
|
return readSlot(
|
|
493
|
-
this.arrayTable
|
|
512
|
+
this.arrayTable,
|
|
494
513
|
ref.offset + ARRAY_HEADER + index * SLOT_SIZE,
|
|
495
514
|
);
|
|
496
515
|
}
|
|
497
516
|
|
|
498
517
|
/** Read the stable objectId of an array block in the ArrayTable. */
|
|
499
518
|
getArrayObjectId(offset: number): number {
|
|
500
|
-
return this.arrayTable.
|
|
519
|
+
return this.arrayTable.getInt32(offset);
|
|
501
520
|
}
|
|
502
521
|
|
|
503
522
|
/**
|
|
@@ -510,14 +529,13 @@ export class SharedMemory {
|
|
|
510
529
|
this.arrayTable.ensureCapacity(
|
|
511
530
|
newOffset + ARRAY_HEADER + newCapacity * SLOT_SIZE,
|
|
512
531
|
);
|
|
513
|
-
const view = this.arrayTable.getView();
|
|
514
532
|
const objectId = this.getArrayObjectId(ref.offset);
|
|
515
|
-
|
|
533
|
+
this.arrayTable.setInt32(newOffset, objectId);
|
|
516
534
|
for (let i = 0; i < ref.count; i++) {
|
|
517
535
|
writeSlot(
|
|
518
|
-
|
|
536
|
+
this.arrayTable,
|
|
519
537
|
newOffset + ARRAY_HEADER + i * SLOT_SIZE,
|
|
520
|
-
readSlot(
|
|
538
|
+
readSlot(this.arrayTable, ref.offset + ARRAY_HEADER + i * SLOT_SIZE),
|
|
521
539
|
);
|
|
522
540
|
}
|
|
523
541
|
this.arrayTable.advance(ARRAY_HEADER + newCapacity * SLOT_SIZE);
|
|
@@ -533,7 +551,7 @@ export class SharedMemory {
|
|
|
533
551
|
appendArraySlot(ref: ArrayRef, slot: ValueSlot): ArrayRef {
|
|
534
552
|
if (ref.count < ref.capacity) {
|
|
535
553
|
writeSlot(
|
|
536
|
-
this.arrayTable
|
|
554
|
+
this.arrayTable,
|
|
537
555
|
ref.offset + ARRAY_HEADER + ref.count * SLOT_SIZE,
|
|
538
556
|
slot,
|
|
539
557
|
);
|
|
@@ -545,7 +563,7 @@ export class SharedMemory {
|
|
|
545
563
|
}
|
|
546
564
|
const moved = this.moveArray(ref, Math.max(ref.capacity * 2, 1));
|
|
547
565
|
writeSlot(
|
|
548
|
-
this.arrayTable
|
|
566
|
+
this.arrayTable,
|
|
549
567
|
moved.offset + ARRAY_HEADER + moved.count * SLOT_SIZE,
|
|
550
568
|
slot,
|
|
551
569
|
);
|
|
@@ -564,19 +582,19 @@ export class SharedMemory {
|
|
|
564
582
|
storeValueSlot(slot: ValueSlot): number {
|
|
565
583
|
const offset = this.dataTable.byteLength;
|
|
566
584
|
this.dataTable.ensureCapacity(offset + SLOT_SIZE);
|
|
567
|
-
writeSlot(this.dataTable
|
|
585
|
+
writeSlot(this.dataTable, offset, slot);
|
|
568
586
|
this.dataTable.advance(SLOT_SIZE);
|
|
569
587
|
return offset;
|
|
570
588
|
}
|
|
571
589
|
|
|
572
590
|
/** Read a single value slot from the DataTable. */
|
|
573
591
|
readValueSlot(offset: number): ValueSlot {
|
|
574
|
-
return readSlot(this.dataTable
|
|
592
|
+
return readSlot(this.dataTable, offset);
|
|
575
593
|
}
|
|
576
594
|
|
|
577
595
|
/** Update a single value slot in the DataTable. */
|
|
578
596
|
updateValueSlot(offset: number, slot: ValueSlot): void {
|
|
579
|
-
writeSlot(this.dataTable
|
|
597
|
+
writeSlot(this.dataTable, offset, slot);
|
|
580
598
|
}
|
|
581
599
|
|
|
582
600
|
/**
|
|
@@ -590,20 +608,19 @@ export class SharedMemory {
|
|
|
590
608
|
OBJECT_HEADER + properties.length * (NAME_REF_SIZE + SLOT_SIZE);
|
|
591
609
|
const offset = this.dataTable.byteLength;
|
|
592
610
|
this.dataTable.ensureCapacity(offset + blockSize);
|
|
593
|
-
const view = this.dataTable.getView();
|
|
594
611
|
|
|
595
612
|
const objectId = this.nextObjectId++;
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
613
|
+
this.dataTable.setInt32(offset, TYPE_OBJECT);
|
|
614
|
+
this.dataTable.setInt32(offset + 4, properties.length);
|
|
615
|
+
this.dataTable.setInt32(offset + 8, objectId);
|
|
599
616
|
|
|
600
617
|
let cursor = offset + OBJECT_HEADER;
|
|
601
618
|
for (const [name, slot] of properties) {
|
|
602
619
|
const nameRef = this.storeString(name);
|
|
603
|
-
|
|
604
|
-
|
|
620
|
+
this.dataTable.setInt32(cursor, nameRef.offset);
|
|
621
|
+
this.dataTable.setInt32(cursor + 4, nameRef.length);
|
|
605
622
|
cursor += NAME_REF_SIZE;
|
|
606
|
-
writeSlot(
|
|
623
|
+
writeSlot(this.dataTable, cursor, slot);
|
|
607
624
|
cursor += SLOT_SIZE;
|
|
608
625
|
}
|
|
609
626
|
this.dataTable.advance(blockSize);
|
|
@@ -612,16 +629,15 @@ export class SharedMemory {
|
|
|
612
629
|
|
|
613
630
|
/** Read an object's properties from the DataTable. */
|
|
614
631
|
readObject(offset: number): Array<[string, ValueSlot]> {
|
|
615
|
-
const
|
|
616
|
-
const count = view.getInt32(offset + 4, true);
|
|
632
|
+
const count = this.dataTable.getInt32(offset + 4);
|
|
617
633
|
const properties: Array<[string, ValueSlot]> = [];
|
|
618
634
|
let cursor = offset + OBJECT_HEADER;
|
|
619
635
|
for (let i = 0; i < count; i++) {
|
|
620
|
-
const nameOffset =
|
|
621
|
-
const nameLength =
|
|
636
|
+
const nameOffset = this.dataTable.getInt32(cursor);
|
|
637
|
+
const nameLength = this.dataTable.getInt32(cursor + 4);
|
|
622
638
|
const name = this.readString({ offset: nameOffset, length: nameLength });
|
|
623
639
|
cursor += NAME_REF_SIZE;
|
|
624
|
-
const slot = readSlot(
|
|
640
|
+
const slot = readSlot(this.dataTable, cursor);
|
|
625
641
|
cursor += SLOT_SIZE;
|
|
626
642
|
properties.push([name, slot]);
|
|
627
643
|
}
|
|
@@ -630,7 +646,7 @@ export class SharedMemory {
|
|
|
630
646
|
|
|
631
647
|
/** Read the stable objectId of an object block in the DataTable. */
|
|
632
648
|
getObjectId(offset: number): number {
|
|
633
|
-
return this.dataTable.
|
|
649
|
+
return this.dataTable.getInt32(offset + 8);
|
|
634
650
|
}
|
|
635
651
|
|
|
636
652
|
/**
|
|
@@ -647,14 +663,13 @@ export class SharedMemory {
|
|
|
647
663
|
getObjectLayout(key: string): ObjectLayout | null {
|
|
648
664
|
const offset = this.lookup(key);
|
|
649
665
|
if (offset === null) return null;
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
const count = view.getInt32(offset + 4, true);
|
|
666
|
+
if (this.dataTable.getInt32(offset) !== TYPE_OBJECT) return null;
|
|
667
|
+
const count = this.dataTable.getInt32(offset + 4);
|
|
653
668
|
const properties = new Map<string, number>();
|
|
654
669
|
let cursor = offset + OBJECT_HEADER;
|
|
655
670
|
for (let i = 0; i < count; i++) {
|
|
656
|
-
const nameOffset =
|
|
657
|
-
const nameLength =
|
|
671
|
+
const nameOffset = this.dataTable.getInt32(cursor);
|
|
672
|
+
const nameLength = this.dataTable.getInt32(cursor + 4);
|
|
658
673
|
const name = this.readString({ offset: nameOffset, length: nameLength });
|
|
659
674
|
cursor += NAME_REF_SIZE;
|
|
660
675
|
properties.set(name, cursor);
|
|
@@ -669,13 +684,12 @@ export class SharedMemory {
|
|
|
669
684
|
propertyIndex: number,
|
|
670
685
|
slot: ValueSlot,
|
|
671
686
|
): void {
|
|
672
|
-
const view = this.dataTable.getView();
|
|
673
687
|
const slotOffset =
|
|
674
688
|
offset +
|
|
675
689
|
OBJECT_HEADER +
|
|
676
690
|
propertyIndex * (NAME_REF_SIZE + SLOT_SIZE) +
|
|
677
691
|
NAME_REF_SIZE;
|
|
678
|
-
writeSlot(
|
|
692
|
+
writeSlot(this.dataTable, slotOffset, slot);
|
|
679
693
|
}
|
|
680
694
|
|
|
681
695
|
// ---------------------------------------------------------------------------
|
|
@@ -691,11 +705,10 @@ export class SharedMemory {
|
|
|
691
705
|
|
|
692
706
|
/** Read the DataTable offset for a known stable index, or null if absent. */
|
|
693
707
|
lookupByIndex(index: number): number | null {
|
|
694
|
-
const
|
|
695
|
-
const count = view.getInt32(0, true);
|
|
708
|
+
const count = this.indexTable.getInt32(0);
|
|
696
709
|
if (index >= count) return null;
|
|
697
710
|
const entryOffset = INDEX_HEADER + index * INDEX_ENTRY_SIZE;
|
|
698
|
-
return
|
|
711
|
+
return this.indexTable.getInt32(entryOffset + 8);
|
|
699
712
|
}
|
|
700
713
|
|
|
701
714
|
/**
|
|
@@ -706,12 +719,11 @@ export class SharedMemory {
|
|
|
706
719
|
* used to address the key's version cell.
|
|
707
720
|
*/
|
|
708
721
|
lookupIndex(key: string): number | null {
|
|
709
|
-
const
|
|
710
|
-
const count = view.getInt32(0, true);
|
|
722
|
+
const count = this.indexTable.getInt32(0);
|
|
711
723
|
for (let i = 0; i < count; i++) {
|
|
712
724
|
const entryOffset = INDEX_HEADER + i * INDEX_ENTRY_SIZE;
|
|
713
|
-
const keyOffset =
|
|
714
|
-
const keyLength =
|
|
725
|
+
const keyOffset = this.indexTable.getInt32(entryOffset);
|
|
726
|
+
const keyLength = this.indexTable.getInt32(entryOffset + 4);
|
|
715
727
|
const storedKey = this.readString({
|
|
716
728
|
offset: keyOffset,
|
|
717
729
|
length: keyLength,
|
|
@@ -736,21 +748,21 @@ export class SharedMemory {
|
|
|
736
748
|
/** Insert a key → dataOffset mapping into the Index Table. */
|
|
737
749
|
insert(key: string, dataOffset: number): void {
|
|
738
750
|
const keyRef = this.storeString(key);
|
|
739
|
-
const
|
|
740
|
-
const count = view.getInt32(0, true);
|
|
751
|
+
const count = this.indexTable.getInt32(0);
|
|
741
752
|
const entryOffset = INDEX_HEADER + count * INDEX_ENTRY_SIZE;
|
|
742
753
|
this.indexTable.ensureCapacity(entryOffset + INDEX_ENTRY_SIZE);
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
754
|
+
this.indexTable.setInt32(entryOffset, keyRef.offset);
|
|
755
|
+
this.indexTable.setInt32(entryOffset + 4, keyRef.length);
|
|
756
|
+
this.indexTable.setInt32(entryOffset + 8, dataOffset);
|
|
746
757
|
// The 4th field is the key's version cell, initialized to 0.
|
|
747
|
-
|
|
748
|
-
|
|
758
|
+
this.indexTable.setInt32(entryOffset + INDEX_VERSION_OFFSET, 0);
|
|
759
|
+
this.indexTable.setInt32(0, count + 1);
|
|
749
760
|
this.indexTable.advance(INDEX_ENTRY_SIZE);
|
|
750
761
|
|
|
751
|
-
// Rebuild the version view
|
|
752
|
-
//
|
|
753
|
-
|
|
762
|
+
// Rebuild the version view over the first chunk (the Index Table is small
|
|
763
|
+
// and stays within the first chunk for typical workloads; version cells
|
|
764
|
+
// beyond the first chunk are not addressable via the single Int32Array).
|
|
765
|
+
this.versionView = new Int32Array(this.indexTable.buffers[0]!);
|
|
754
766
|
}
|
|
755
767
|
|
|
756
768
|
// ---------------------------------------------------------------------------
|
|
@@ -812,9 +824,10 @@ export class SharedMemory {
|
|
|
812
824
|
const markArray = (offset: number, count: number) => {
|
|
813
825
|
if (liveArrays.has(offset)) return;
|
|
814
826
|
liveArrays.add(offset);
|
|
815
|
-
const view = this.arrayTable.getView();
|
|
816
827
|
for (let i = 0; i < count; i++) {
|
|
817
|
-
markValueSlot(
|
|
828
|
+
markValueSlot(
|
|
829
|
+
readSlot(this.arrayTable, offset + ARRAY_HEADER + i * SLOT_SIZE),
|
|
830
|
+
);
|
|
818
831
|
}
|
|
819
832
|
};
|
|
820
833
|
|
|
@@ -822,40 +835,37 @@ export class SharedMemory {
|
|
|
822
835
|
if (liveObjects.has(offset)) return;
|
|
823
836
|
liveObjects.add(offset);
|
|
824
837
|
liveDataOffsets.add(offset);
|
|
825
|
-
const
|
|
826
|
-
const count = view.getInt32(offset + 4, true);
|
|
838
|
+
const count = this.dataTable.getInt32(offset + 4);
|
|
827
839
|
let cursor = offset + OBJECT_HEADER;
|
|
828
840
|
for (let i = 0; i < count; i++) {
|
|
829
|
-
const nameOffset =
|
|
830
|
-
const nameLength =
|
|
841
|
+
const nameOffset = this.dataTable.getInt32(cursor);
|
|
842
|
+
const nameLength = this.dataTable.getInt32(cursor + 4);
|
|
831
843
|
liveStrings.add(nameOffset);
|
|
832
844
|
cursor += NAME_REF_SIZE;
|
|
833
|
-
markValueSlot(readSlot(
|
|
845
|
+
markValueSlot(readSlot(this.dataTable, cursor));
|
|
834
846
|
cursor += SLOT_SIZE;
|
|
835
847
|
}
|
|
836
848
|
};
|
|
837
849
|
|
|
838
850
|
const markValue = (dataOffset: number) => {
|
|
839
|
-
|
|
840
|
-
if (view.getInt32(dataOffset, true) === TYPE_OBJECT) {
|
|
851
|
+
if (this.dataTable.getInt32(dataOffset) === TYPE_OBJECT) {
|
|
841
852
|
markObject(dataOffset);
|
|
842
853
|
} else {
|
|
843
854
|
liveDataOffsets.add(dataOffset);
|
|
844
|
-
markValueSlot(readSlot(
|
|
855
|
+
markValueSlot(readSlot(this.dataTable, dataOffset));
|
|
845
856
|
}
|
|
846
857
|
};
|
|
847
858
|
|
|
848
859
|
// Phase 1: Mark live data by walking the Index Table.
|
|
849
|
-
const
|
|
850
|
-
const indexCount = indexView.getInt32(0, true);
|
|
860
|
+
const indexCount = this.indexTable.getInt32(0);
|
|
851
861
|
const indexEntries: Array<{ keyRef: StringRef; dataOffset: number }> = [];
|
|
852
862
|
for (let i = 0; i < indexCount; i++) {
|
|
853
863
|
const entryOffset = INDEX_HEADER + i * INDEX_ENTRY_SIZE;
|
|
854
864
|
const keyRef = {
|
|
855
|
-
offset:
|
|
856
|
-
length:
|
|
865
|
+
offset: this.indexTable.getInt32(entryOffset),
|
|
866
|
+
length: this.indexTable.getInt32(entryOffset + 4),
|
|
857
867
|
};
|
|
858
|
-
const dataOffset =
|
|
868
|
+
const dataOffset = this.indexTable.getInt32(entryOffset + 8);
|
|
859
869
|
indexEntries.push({ keyRef, dataOffset });
|
|
860
870
|
liveStrings.add(keyRef.offset);
|
|
861
871
|
markValue(dataOffset);
|
|
@@ -864,14 +874,13 @@ export class SharedMemory {
|
|
|
864
874
|
// Phase 2: Compact the DataTable (objects and single value slots).
|
|
865
875
|
const sortedData = [...liveDataOffsets].sort((a, b) => a - b);
|
|
866
876
|
let newDataOffset = 0;
|
|
867
|
-
const dataView = this.dataTable.getView();
|
|
868
877
|
for (const oldOffset of sortedData) {
|
|
869
878
|
const size =
|
|
870
|
-
|
|
879
|
+
this.dataTable.getInt32(oldOffset) === TYPE_OBJECT
|
|
871
880
|
? OBJECT_HEADER +
|
|
872
|
-
|
|
881
|
+
this.dataTable.getInt32(oldOffset + 4) * (NAME_REF_SIZE + SLOT_SIZE)
|
|
873
882
|
: SLOT_SIZE;
|
|
874
|
-
copyBytes(
|
|
883
|
+
copyBytes(this.dataTable, newDataOffset, oldOffset, size);
|
|
875
884
|
objectRemap.set(oldOffset, newDataOffset);
|
|
876
885
|
newDataOffset += size;
|
|
877
886
|
}
|
|
@@ -880,7 +889,6 @@ export class SharedMemory {
|
|
|
880
889
|
// Phase 3: Compact the ArrayTable.
|
|
881
890
|
const sortedArrays = [...liveArrays].sort((a, b) => a - b);
|
|
882
891
|
let newArrayOffset = 0;
|
|
883
|
-
const arrayView = this.arrayTable.getView();
|
|
884
892
|
for (const oldOffset of sortedArrays) {
|
|
885
893
|
// Determine the array's capacity by scanning forward to the next live
|
|
886
894
|
// array or the end of the used region.
|
|
@@ -888,7 +896,7 @@ export class SharedMemory {
|
|
|
888
896
|
const end = next !== undefined ? next : this.arrayTable.byteLength;
|
|
889
897
|
const capacity = (end - oldOffset - ARRAY_HEADER) / SLOT_SIZE;
|
|
890
898
|
copyBytes(
|
|
891
|
-
|
|
899
|
+
this.arrayTable,
|
|
892
900
|
newArrayOffset,
|
|
893
901
|
oldOffset,
|
|
894
902
|
ARRAY_HEADER + capacity * SLOT_SIZE,
|
|
@@ -901,12 +909,11 @@ export class SharedMemory {
|
|
|
901
909
|
// Phase 4: Compact the StringTable.
|
|
902
910
|
const sortedStrings = [...liveStrings].sort((a, b) => a - b);
|
|
903
911
|
let newStringOffset = 0;
|
|
904
|
-
const stringView = this.stringTable.getView();
|
|
905
912
|
for (const oldOffset of sortedStrings) {
|
|
906
913
|
const next = sortedStrings[sortedStrings.indexOf(oldOffset) + 1];
|
|
907
914
|
const end = next !== undefined ? next : this.stringTable.byteLength;
|
|
908
915
|
const length = end - oldOffset;
|
|
909
|
-
copyBytes(
|
|
916
|
+
copyBytes(this.stringTable, newStringOffset, oldOffset, length);
|
|
910
917
|
stringRemap.set(oldOffset, newStringOffset);
|
|
911
918
|
newStringOffset += length;
|
|
912
919
|
}
|
|
@@ -916,29 +923,29 @@ export class SharedMemory {
|
|
|
916
923
|
for (let i = 0; i < indexCount; i++) {
|
|
917
924
|
const entryOffset = INDEX_HEADER + i * INDEX_ENTRY_SIZE;
|
|
918
925
|
const { keyRef, dataOffset } = indexEntries[i]!;
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
926
|
+
this.indexTable.setInt32(entryOffset, stringRemap.get(keyRef.offset)!);
|
|
927
|
+
this.indexTable.setInt32(entryOffset + 4, keyRef.length);
|
|
928
|
+
this.indexTable.setInt32(entryOffset + 8, objectRemap.get(dataOffset)!);
|
|
922
929
|
}
|
|
923
930
|
|
|
924
931
|
// Phase 6: Rewrite object property names and value slots, and array slots.
|
|
925
932
|
for (const oldOffset of sortedData) {
|
|
926
|
-
if (
|
|
933
|
+
if (this.dataTable.getInt32(oldOffset) !== TYPE_OBJECT) continue;
|
|
927
934
|
const newOff = objectRemap.get(oldOffset)!;
|
|
928
|
-
const count =
|
|
935
|
+
const count = this.dataTable.getInt32(newOff + 4);
|
|
929
936
|
let cursor = newOff + OBJECT_HEADER;
|
|
930
937
|
for (let i = 0; i < count; i++) {
|
|
931
|
-
const nameOffset =
|
|
932
|
-
|
|
938
|
+
const nameOffset = this.dataTable.getInt32(cursor);
|
|
939
|
+
this.dataTable.setInt32(cursor, stringRemap.get(nameOffset)!);
|
|
933
940
|
cursor += NAME_REF_SIZE;
|
|
934
|
-
remapSlot(
|
|
941
|
+
remapSlot(this.dataTable, cursor, stringRemap, arrayRemap);
|
|
935
942
|
cursor += SLOT_SIZE;
|
|
936
943
|
}
|
|
937
944
|
}
|
|
938
945
|
for (const oldOffset of sortedData) {
|
|
939
|
-
if (
|
|
946
|
+
if (this.dataTable.getInt32(oldOffset) === TYPE_OBJECT) continue;
|
|
940
947
|
const newOff = objectRemap.get(oldOffset)!;
|
|
941
|
-
remapSlot(
|
|
948
|
+
remapSlot(this.dataTable, newOff, stringRemap, arrayRemap);
|
|
942
949
|
}
|
|
943
950
|
for (const oldOffset of sortedArrays) {
|
|
944
951
|
const newOff = arrayRemap.get(oldOffset)!;
|
|
@@ -947,7 +954,7 @@ export class SharedMemory {
|
|
|
947
954
|
const capacity = (end - oldOffset - ARRAY_HEADER) / SLOT_SIZE;
|
|
948
955
|
for (let i = 0; i < capacity; i++) {
|
|
949
956
|
remapSlot(
|
|
950
|
-
|
|
957
|
+
this.arrayTable,
|
|
951
958
|
newOff + ARRAY_HEADER + i * SLOT_SIZE,
|
|
952
959
|
stringRemap,
|
|
953
960
|
arrayRemap,
|
|
@@ -960,31 +967,31 @@ export class SharedMemory {
|
|
|
960
967
|
}
|
|
961
968
|
}
|
|
962
969
|
|
|
963
|
-
function writeSlot(
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
970
|
+
function writeSlot(buf: GrowableBuffer, offset: number, slot: ValueSlot): void {
|
|
971
|
+
buf.setInt32(offset, slot.type);
|
|
972
|
+
buf.setInt32(offset + 4, slot.a);
|
|
973
|
+
buf.setInt32(offset + 8, slot.b);
|
|
974
|
+
buf.setInt32(offset + 12, slot.c);
|
|
968
975
|
}
|
|
969
976
|
|
|
970
|
-
function readSlot(
|
|
977
|
+
function readSlot(buf: GrowableBuffer, offset: number): ValueSlot {
|
|
971
978
|
return {
|
|
972
|
-
type:
|
|
973
|
-
a:
|
|
974
|
-
b:
|
|
975
|
-
c:
|
|
979
|
+
type: buf.getInt32(offset),
|
|
980
|
+
a: buf.getInt32(offset + 4),
|
|
981
|
+
b: buf.getInt32(offset + 8),
|
|
982
|
+
c: buf.getInt32(offset + 12),
|
|
976
983
|
};
|
|
977
984
|
}
|
|
978
985
|
|
|
979
986
|
/** Copy `len` bytes from `src` to `dst` in the same buffer (dst <= src). */
|
|
980
987
|
function copyBytes(
|
|
981
|
-
|
|
988
|
+
buf: GrowableBuffer,
|
|
982
989
|
dst: number,
|
|
983
990
|
src: number,
|
|
984
991
|
len: number,
|
|
985
992
|
): void {
|
|
986
993
|
for (let i = 0; i < len; i++) {
|
|
987
|
-
|
|
994
|
+
buf.setUint8(dst + i, buf.getUint8(src + i));
|
|
988
995
|
}
|
|
989
996
|
}
|
|
990
997
|
|
|
@@ -994,23 +1001,15 @@ function copyBytes(
|
|
|
994
1001
|
* via `arrayRemap`.
|
|
995
1002
|
*/
|
|
996
1003
|
function remapSlot(
|
|
997
|
-
|
|
1004
|
+
buf: GrowableBuffer,
|
|
998
1005
|
offset: number,
|
|
999
1006
|
stringRemap: Map<number, number>,
|
|
1000
1007
|
arrayRemap: Map<number, number>,
|
|
1001
1008
|
): void {
|
|
1002
|
-
const type =
|
|
1009
|
+
const type = buf.getInt32(offset);
|
|
1003
1010
|
if (type === TYPE_STRING_REF || type === TYPE_BIGINT_STR) {
|
|
1004
|
-
|
|
1005
|
-
offset + 4,
|
|
1006
|
-
stringRemap.get(view.getInt32(offset + 4, true))!,
|
|
1007
|
-
true,
|
|
1008
|
-
);
|
|
1011
|
+
buf.setInt32(offset + 4, stringRemap.get(buf.getInt32(offset + 4))!);
|
|
1009
1012
|
} else if (type === TYPE_ARRAY_REF) {
|
|
1010
|
-
|
|
1011
|
-
offset + 4,
|
|
1012
|
-
arrayRemap.get(view.getInt32(offset + 4, true))!,
|
|
1013
|
-
true,
|
|
1014
|
-
);
|
|
1013
|
+
buf.setInt32(offset + 4, arrayRemap.get(buf.getInt32(offset + 4))!);
|
|
1015
1014
|
}
|
|
1016
1015
|
}
|
package/src/refcount.ts
CHANGED
|
@@ -286,9 +286,9 @@ export class RefCountCoordinator {
|
|
|
286
286
|
|
|
287
287
|
/** Whether any table has less than `defragThreshold` free space. */
|
|
288
288
|
needsDefrag(): boolean {
|
|
289
|
-
const max = this.mem.dataTable.
|
|
290
|
-
const free = (b: {
|
|
291
|
-
(b.
|
|
289
|
+
const max = this.mem.dataTable.capacity;
|
|
290
|
+
const free = (b: { capacity: number; byteLength: number }) =>
|
|
291
|
+
(b.capacity - b.byteLength) / max;
|
|
292
292
|
return (
|
|
293
293
|
free(this.mem.dataTable) < this.defragThreshold ||
|
|
294
294
|
free(this.mem.arrayTable) < this.defragThreshold ||
|
package/src/shared.ts
CHANGED
|
@@ -178,7 +178,7 @@ function isTrackable(value: unknown): boolean {
|
|
|
178
178
|
function getTopLevelObjectId(mem: SharedMemory, key: string): number | null {
|
|
179
179
|
const offset = getOffset(mem, key);
|
|
180
180
|
if (offset === null) return null;
|
|
181
|
-
const type = mem.dataTable.
|
|
181
|
+
const type = mem.dataTable.getInt32(offset);
|
|
182
182
|
if (type === 5) {
|
|
183
183
|
return mem.getObjectId(offset);
|
|
184
184
|
}
|
|
@@ -517,7 +517,7 @@ function createSharedRef<T>(
|
|
|
517
517
|
enumerable: false,
|
|
518
518
|
configurable: false,
|
|
519
519
|
get() {
|
|
520
|
-
return mem.dataTable.
|
|
520
|
+
return mem.dataTable.buffers[0];
|
|
521
521
|
},
|
|
522
522
|
});
|
|
523
523
|
const ref = Object.create(proto) as Record<string | symbol, unknown>;
|
|
@@ -564,7 +564,7 @@ function createSharedRef<T>(
|
|
|
564
564
|
if (prop === "write") return t.write;
|
|
565
565
|
if (prop === "update") return t.update;
|
|
566
566
|
if (prop === "key") return t.key;
|
|
567
|
-
if (prop === "buffer") return mem.dataTable.
|
|
567
|
+
if (prop === "buffer") return mem.dataTable.buffers[0];
|
|
568
568
|
if (prop === Symbol.toPrimitive) return () => t.read();
|
|
569
569
|
|
|
570
570
|
// push/pop on a top-level array: operate directly on the array ref
|
|
@@ -635,7 +635,7 @@ function readValue(
|
|
|
635
635
|
if (offset === null) {
|
|
636
636
|
throw new Error(`No shared value found for key "${key}"`);
|
|
637
637
|
}
|
|
638
|
-
const type = mem.dataTable.
|
|
638
|
+
const type = mem.dataTable.getInt32(offset);
|
|
639
639
|
if (type === 5) {
|
|
640
640
|
// Object block.
|
|
641
641
|
const properties = mem.readObject(offset);
|
|
@@ -665,7 +665,7 @@ function writeValue(
|
|
|
665
665
|
mem.withLock(() => {
|
|
666
666
|
const existing = getOffset(mem, key);
|
|
667
667
|
if (existing !== null) {
|
|
668
|
-
const type = mem.dataTable.
|
|
668
|
+
const type = mem.dataTable.getInt32(existing);
|
|
669
669
|
if (type === 5) {
|
|
670
670
|
// Update existing object's property slots in place.
|
|
671
671
|
const properties = mem.readObject(existing);
|
package/src/worker.ts
CHANGED
|
@@ -144,6 +144,7 @@ function stopCoordinator(): void {
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
147
148
|
/** The main-thread worker listener, so it can be removed by `stopInit()`. */
|
|
148
149
|
let mainListener:
|
|
149
150
|
| ((worker: {
|
|
@@ -166,12 +167,17 @@ function initMain(): void {
|
|
|
166
167
|
}) => {
|
|
167
168
|
worker.addEventListener("message", (e) => {
|
|
168
169
|
const msg = e.data as { __sharedMemoryRequest?: boolean } | undefined;
|
|
169
|
-
if (
|
|
170
|
+
if (
|
|
171
|
+
msg !== null &&
|
|
172
|
+
typeof msg === "object" &&
|
|
173
|
+
"__sharedMemoryRequest" in msg
|
|
174
|
+
) {
|
|
170
175
|
const mem = getDefaultMemory();
|
|
171
176
|
worker.postMessage({
|
|
172
177
|
__sharedMemoryInit: true,
|
|
173
178
|
buffers: mem.exportBuffers(),
|
|
174
179
|
});
|
|
180
|
+
return;
|
|
175
181
|
}
|
|
176
182
|
});
|
|
177
183
|
};
|
|
@@ -234,7 +240,12 @@ function installMessageWrapper(): void {
|
|
|
234
240
|
buffers?: Parameters<typeof SharedMemory.fromBuffers>[0];
|
|
235
241
|
}
|
|
236
242
|
| undefined;
|
|
237
|
-
if (
|
|
243
|
+
if (
|
|
244
|
+
msg !== null &&
|
|
245
|
+
typeof msg === "object" &&
|
|
246
|
+
"__sharedMemoryInit" in msg &&
|
|
247
|
+
msg.buffers
|
|
248
|
+
) {
|
|
238
249
|
const mem = SharedMemory.fromBuffers(msg.buffers);
|
|
239
250
|
setDefaultMemory(mem);
|
|
240
251
|
// Create the worker-side refcount tracker from the shared refcount
|