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/refcount.ts
CHANGED
|
@@ -161,6 +161,13 @@ export class RefCountCoordinator {
|
|
|
161
161
|
/** Free-space threshold (fraction of maxByteLength) that triggers defrag. */
|
|
162
162
|
private readonly defragThreshold: number;
|
|
163
163
|
private maintenanceTimer: ReturnType<typeof setInterval> | null = null;
|
|
164
|
+
/** Called after a defragmentation pass with the new chunk lists. */
|
|
165
|
+
private readonly onDefragment?: (buffers: {
|
|
166
|
+
indexTable: SharedArrayBuffer[];
|
|
167
|
+
dataTable: SharedArrayBuffer[];
|
|
168
|
+
stringTable: SharedArrayBuffer[];
|
|
169
|
+
arrayTable: SharedArrayBuffer[];
|
|
170
|
+
}) => void;
|
|
164
171
|
|
|
165
172
|
constructor(
|
|
166
173
|
mem: SharedMemory,
|
|
@@ -169,9 +176,16 @@ export class RefCountCoordinator {
|
|
|
169
176
|
defragThreshold?: number;
|
|
170
177
|
autoMaintenance?: boolean;
|
|
171
178
|
maintenanceInterval?: number;
|
|
179
|
+
onDefragment?: (buffers: {
|
|
180
|
+
indexTable: SharedArrayBuffer[];
|
|
181
|
+
dataTable: SharedArrayBuffer[];
|
|
182
|
+
stringTable: SharedArrayBuffer[];
|
|
183
|
+
arrayTable: SharedArrayBuffer[];
|
|
184
|
+
}) => void;
|
|
172
185
|
} = {},
|
|
173
186
|
) {
|
|
174
187
|
this.mem = mem;
|
|
188
|
+
this.onDefragment = opts.onDefragment;
|
|
175
189
|
// Reuse the memory's shared refCount buffer if present, so the same SAB is
|
|
176
190
|
// exported to workers; otherwise create a fresh one and attach it to the
|
|
177
191
|
// memory so it can be shared.
|
|
@@ -286,9 +300,9 @@ export class RefCountCoordinator {
|
|
|
286
300
|
|
|
287
301
|
/** Whether any table has less than `defragThreshold` free space. */
|
|
288
302
|
needsDefrag(): boolean {
|
|
289
|
-
const max = this.mem.dataTable.
|
|
290
|
-
const free = (b: {
|
|
291
|
-
(b.
|
|
303
|
+
const max = this.mem.dataTable.capacity;
|
|
304
|
+
const free = (b: { capacity: number; byteLength: number }) =>
|
|
305
|
+
(b.capacity - b.byteLength) / max;
|
|
292
306
|
return (
|
|
293
307
|
free(this.mem.dataTable) < this.defragThreshold ||
|
|
294
308
|
free(this.mem.arrayTable) < this.defragThreshold ||
|
|
@@ -313,7 +327,8 @@ export class RefCountCoordinator {
|
|
|
313
327
|
this.maintenanceTimer = setInterval(() => {
|
|
314
328
|
this.drainEvents();
|
|
315
329
|
if (this.needsDefrag()) {
|
|
316
|
-
this.mem.withLock(() => this.mem.defragment());
|
|
330
|
+
const buffers = this.mem.withLock(() => this.mem.defragment());
|
|
331
|
+
this.onDefragment?.(buffers);
|
|
317
332
|
}
|
|
318
333
|
}, intervalMs);
|
|
319
334
|
// Unref the timer so it does not keep the process alive. The coordinator
|
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
|
@@ -130,7 +130,9 @@ export function getDefaultTracker(): RefCountTracker | null {
|
|
|
130
130
|
function startCoordinator(): void {
|
|
131
131
|
if (coordinator !== null) return;
|
|
132
132
|
const mem = getDefaultMemory();
|
|
133
|
-
coordinator = new RefCountCoordinator(mem
|
|
133
|
+
coordinator = new RefCountCoordinator(mem, undefined, {
|
|
134
|
+
onDefragment: (buffers) => broadcastChunkUpdate(buffers),
|
|
135
|
+
});
|
|
134
136
|
unregisterWorkers = coordinator.autoRegister();
|
|
135
137
|
}
|
|
136
138
|
|
|
@@ -144,6 +146,7 @@ function stopCoordinator(): void {
|
|
|
144
146
|
}
|
|
145
147
|
}
|
|
146
148
|
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
147
150
|
/** The main-thread worker listener, so it can be removed by `stopInit()`. */
|
|
148
151
|
let mainListener:
|
|
149
152
|
| ((worker: {
|
|
@@ -155,6 +158,12 @@ let mainListener:
|
|
|
155
158
|
}) => void)
|
|
156
159
|
| null = null;
|
|
157
160
|
|
|
161
|
+
/** Live workers on the main thread, for broadcasting chunk updates. */
|
|
162
|
+
const liveWorkers = new Set<{
|
|
163
|
+
postMessage: (msg: unknown) => void;
|
|
164
|
+
addEventListener: (type: string, fn: (e: { data: unknown }) => void) => void;
|
|
165
|
+
}>();
|
|
166
|
+
|
|
158
167
|
/** Main thread: answer each worker's buffer request. */
|
|
159
168
|
function initMain(): void {
|
|
160
169
|
const listener = (worker: {
|
|
@@ -164,14 +173,20 @@ function initMain(): void {
|
|
|
164
173
|
fn: (e: { data: unknown }) => void,
|
|
165
174
|
) => void;
|
|
166
175
|
}) => {
|
|
176
|
+
liveWorkers.add(worker);
|
|
167
177
|
worker.addEventListener("message", (e) => {
|
|
168
178
|
const msg = e.data as { __sharedMemoryRequest?: boolean } | undefined;
|
|
169
|
-
if (
|
|
179
|
+
if (
|
|
180
|
+
msg !== null &&
|
|
181
|
+
typeof msg === "object" &&
|
|
182
|
+
"__sharedMemoryRequest" in msg
|
|
183
|
+
) {
|
|
170
184
|
const mem = getDefaultMemory();
|
|
171
185
|
worker.postMessage({
|
|
172
186
|
__sharedMemoryInit: true,
|
|
173
187
|
buffers: mem.exportBuffers(),
|
|
174
188
|
});
|
|
189
|
+
return;
|
|
175
190
|
}
|
|
176
191
|
});
|
|
177
192
|
};
|
|
@@ -185,6 +200,22 @@ function stopMain(): void {
|
|
|
185
200
|
process.off("worker", mainListener);
|
|
186
201
|
mainListener = null;
|
|
187
202
|
}
|
|
203
|
+
liveWorkers.clear();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Broadcast a chunk update (new chunk lists after a defragmentation pass) to
|
|
208
|
+
* all live workers. Workers apply it via `applyChunkUpdate()`.
|
|
209
|
+
*/
|
|
210
|
+
function broadcastChunkUpdate(buffers: {
|
|
211
|
+
indexTable: SharedArrayBuffer[];
|
|
212
|
+
dataTable: SharedArrayBuffer[];
|
|
213
|
+
stringTable: SharedArrayBuffer[];
|
|
214
|
+
arrayTable: SharedArrayBuffer[];
|
|
215
|
+
}): void {
|
|
216
|
+
for (const worker of liveWorkers) {
|
|
217
|
+
worker.postMessage({ __chunkUpdate: true, buffers });
|
|
218
|
+
}
|
|
188
219
|
}
|
|
189
220
|
|
|
190
221
|
/** Worker: request the buffers from the main thread and await the reply. */
|
|
@@ -233,8 +264,22 @@ function installMessageWrapper(): void {
|
|
|
233
264
|
__sharedMemoryInit?: boolean;
|
|
234
265
|
buffers?: Parameters<typeof SharedMemory.fromBuffers>[0];
|
|
235
266
|
}
|
|
267
|
+
| {
|
|
268
|
+
__chunkUpdate?: boolean;
|
|
269
|
+
buffers?: {
|
|
270
|
+
indexTable: SharedArrayBuffer[];
|
|
271
|
+
dataTable: SharedArrayBuffer[];
|
|
272
|
+
stringTable: SharedArrayBuffer[];
|
|
273
|
+
arrayTable: SharedArrayBuffer[];
|
|
274
|
+
};
|
|
275
|
+
}
|
|
236
276
|
| undefined;
|
|
237
|
-
if (
|
|
277
|
+
if (
|
|
278
|
+
msg !== null &&
|
|
279
|
+
typeof msg === "object" &&
|
|
280
|
+
"__sharedMemoryInit" in msg &&
|
|
281
|
+
msg.buffers
|
|
282
|
+
) {
|
|
238
283
|
const mem = SharedMemory.fromBuffers(msg.buffers);
|
|
239
284
|
setDefaultMemory(mem);
|
|
240
285
|
// Create the worker-side refcount tracker from the shared refcount
|
|
@@ -253,6 +298,18 @@ function installMessageWrapper(): void {
|
|
|
253
298
|
flush();
|
|
254
299
|
return;
|
|
255
300
|
}
|
|
301
|
+
if (
|
|
302
|
+
msg !== null &&
|
|
303
|
+
typeof msg === "object" &&
|
|
304
|
+
"__chunkUpdate" in msg &&
|
|
305
|
+
msg.buffers &&
|
|
306
|
+
hasDefaultMemory()
|
|
307
|
+
) {
|
|
308
|
+
// The main thread replaced inactive chunks after a defragmentation
|
|
309
|
+
// pass; apply the new chunk lists so this worker sees the replacement.
|
|
310
|
+
getDefaultMemory().applyChunkUpdate(msg.buffers);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
256
313
|
if (hasDefaultMemory() && hasHandler) {
|
|
257
314
|
messageHandler(e);
|
|
258
315
|
} else {
|