bun-shared-memory 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +310 -2
- package/index.ts +1 -0
- package/package.json +47 -4
- package/src/growable-buffer.ts +110 -0
- package/src/index.ts +21 -0
- package/src/memory.ts +1016 -0
- package/src/mutex.ts +92 -0
- package/src/notifier.ts +39 -0
- package/src/refcount.ts +358 -0
- package/src/shared.ts +957 -0
- package/src/worker.ts +292 -0
package/src/memory.ts
ADDED
|
@@ -0,0 +1,1016 @@
|
|
|
1
|
+
import { GrowableBuffer, SharedBudget } from "./growable-buffer.ts";
|
|
2
|
+
import { Mutex } from "./mutex.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The 4-buffer shared memory architecture.
|
|
6
|
+
*
|
|
7
|
+
* 1) Index Table — maps object keys → DataTable offsets
|
|
8
|
+
* 2) DataTable — stores objects, arrays, strings as references
|
|
9
|
+
* 3) StringTable — append-only storage for strings
|
|
10
|
+
* 4) ArrayTable — append-only storage for arrays
|
|
11
|
+
*
|
|
12
|
+
* Value slot layout (16 bytes):
|
|
13
|
+
* [0..3] type (Int32): 0=number, 1=boolean, 2=bigint, 3=string-ref, 4=array-ref, 6=bigint-str
|
|
14
|
+
* [4..7] payload A
|
|
15
|
+
* [8..11] payload B
|
|
16
|
+
* [12..15] payload C
|
|
17
|
+
*
|
|
18
|
+
* number -> float64 in [4..11]
|
|
19
|
+
* boolean -> [4] = 0/1
|
|
20
|
+
* bigint -> int64 in [4..11] (only values that fit in 64 bits)
|
|
21
|
+
* bigint-str -> [4..7]=offset, [8..11]=length (into StringTable); for bigints
|
|
22
|
+
* too large for 64 bits, stored as their decimal string
|
|
23
|
+
* string-ref -> [4..7]=offset, [8..11]=length (into StringTable)
|
|
24
|
+
* array-ref -> [4..7]=offset, [8..11]=count (into ArrayTable)
|
|
25
|
+
*
|
|
26
|
+
* Object block layout in DataTable:
|
|
27
|
+
* [0..3] type = OBJECT (5)
|
|
28
|
+
* [4..7] property count
|
|
29
|
+
* [8..11] reserved
|
|
30
|
+
* [12..15] reserved
|
|
31
|
+
* then propertyCount * (nameRef(8 bytes) + valueSlot(16 bytes))
|
|
32
|
+
*
|
|
33
|
+
* Index Table entry (12 bytes):
|
|
34
|
+
* [0..3] key string offset
|
|
35
|
+
* [4..7] key string length
|
|
36
|
+
* [8..11] data offset (into DataTable)
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
export const TYPE_NUMBER = 0;
|
|
40
|
+
export const TYPE_BOOLEAN = 1;
|
|
41
|
+
export const TYPE_BIGINT = 2;
|
|
42
|
+
export const TYPE_STRING_REF = 3;
|
|
43
|
+
export const TYPE_ARRAY_REF = 4;
|
|
44
|
+
export const TYPE_OBJECT = 5;
|
|
45
|
+
export const TYPE_BIGINT_STR = 6;
|
|
46
|
+
|
|
47
|
+
const SLOT_SIZE = 16;
|
|
48
|
+
const OBJECT_HEADER = 16;
|
|
49
|
+
const NAME_REF_SIZE = 8;
|
|
50
|
+
const INDEX_ENTRY_SIZE = 16;
|
|
51
|
+
const INDEX_HEADER = 4;
|
|
52
|
+
/** Byte offset of the version field within an index entry. */
|
|
53
|
+
const INDEX_VERSION_OFFSET = 12;
|
|
54
|
+
/** Size of the objectId header at the start of each array block. */
|
|
55
|
+
const ARRAY_HEADER = 4;
|
|
56
|
+
|
|
57
|
+
const DEFAULT_INITIAL = 1024;
|
|
58
|
+
const DEFAULT_MAX = 64 * 1024 * 1024; // 64 MiB
|
|
59
|
+
const DEFAULT_INTERN_CAPACITY = 256;
|
|
60
|
+
const DEFAULT_INTERN_MAX = 1 << 16; // 65536 entries
|
|
61
|
+
|
|
62
|
+
/** Int32Array index of a key's version cell within the Index Table buffer. */
|
|
63
|
+
function versionCellIndex(index: number): number {
|
|
64
|
+
return (INDEX_HEADER + index * INDEX_ENTRY_SIZE + INDEX_VERSION_OFFSET) / 4;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Reusable DataView for packing/unpacking 64-bit values (number, bigint) into
|
|
69
|
+
* the two 32-bit halves of a ValueSlot. Each worker has its own module
|
|
70
|
+
* instance, and all uses happen under the shared lock (or on a single thread),
|
|
71
|
+
* so reusing one view is safe and avoids allocating a buffer per call.
|
|
72
|
+
*/
|
|
73
|
+
const slotView = new DataView(new ArrayBuffer(8));
|
|
74
|
+
|
|
75
|
+
export interface StringRef {
|
|
76
|
+
offset: number;
|
|
77
|
+
length: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface ArrayRef {
|
|
81
|
+
/**
|
|
82
|
+
* Byte offset of the array block in the ArrayTable, pointing at the 4-byte
|
|
83
|
+
* objectId header. The value slots start at `offset + ARRAY_HEADER`.
|
|
84
|
+
*/
|
|
85
|
+
offset: number;
|
|
86
|
+
count: number;
|
|
87
|
+
/** Number of slots reserved for this array (>= count). */
|
|
88
|
+
capacity: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface ValueSlot {
|
|
92
|
+
type: number;
|
|
93
|
+
a: number;
|
|
94
|
+
b: number;
|
|
95
|
+
c: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The stable layout of an object value in the DataTable: its total offset and
|
|
100
|
+
* the slot offset of each property. Static for the lifetime of the object.
|
|
101
|
+
*/
|
|
102
|
+
export interface ObjectLayout {
|
|
103
|
+
totalOffset: number;
|
|
104
|
+
properties: Map<string, number>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Encode a number (float64) into a ValueSlot. */
|
|
108
|
+
export function numberSlot(value: number): ValueSlot {
|
|
109
|
+
slotView.setFloat64(0, value, true);
|
|
110
|
+
return {
|
|
111
|
+
type: TYPE_NUMBER,
|
|
112
|
+
a: slotView.getInt32(0, true),
|
|
113
|
+
b: slotView.getInt32(4, true),
|
|
114
|
+
c: 0,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Encode a boolean into a ValueSlot. */
|
|
119
|
+
export function booleanSlot(value: boolean): ValueSlot {
|
|
120
|
+
return { type: TYPE_BOOLEAN, a: value ? 1 : 0, b: 0, c: 0 };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Encode a bigint (int64) into a ValueSlot. */
|
|
124
|
+
export function bigintSlot(value: bigint): ValueSlot {
|
|
125
|
+
slotView.setBigInt64(0, value, true);
|
|
126
|
+
return {
|
|
127
|
+
type: TYPE_BIGINT,
|
|
128
|
+
a: slotView.getInt32(0, true),
|
|
129
|
+
b: slotView.getInt32(4, true),
|
|
130
|
+
c: 0,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Encode a string reference into a ValueSlot. */
|
|
135
|
+
export function stringRefSlot(ref: StringRef): ValueSlot {
|
|
136
|
+
return { type: TYPE_STRING_REF, a: ref.offset, b: ref.length, c: 0 };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Encode a bigint stored as a decimal string into a ValueSlot. */
|
|
140
|
+
export function bigintStrSlot(ref: StringRef): ValueSlot {
|
|
141
|
+
return { type: TYPE_BIGINT_STR, a: ref.offset, b: ref.length, c: 0 };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Encode an array reference into a ValueSlot. */
|
|
145
|
+
export function arrayRefSlot(ref: ArrayRef): ValueSlot {
|
|
146
|
+
return { type: TYPE_ARRAY_REF, a: ref.offset, b: ref.count, c: ref.capacity };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Decode a ValueSlot into a JS value (primitives) or a ref object. */
|
|
150
|
+
export function decodeSlot(slot: ValueSlot): unknown {
|
|
151
|
+
switch (slot.type) {
|
|
152
|
+
case TYPE_NUMBER: {
|
|
153
|
+
slotView.setInt32(0, slot.a, true);
|
|
154
|
+
slotView.setInt32(4, slot.b, true);
|
|
155
|
+
return slotView.getFloat64(0, true);
|
|
156
|
+
}
|
|
157
|
+
case TYPE_BOOLEAN:
|
|
158
|
+
return slot.a === 1;
|
|
159
|
+
case TYPE_BIGINT: {
|
|
160
|
+
slotView.setInt32(0, slot.a, true);
|
|
161
|
+
slotView.setInt32(4, slot.b, true);
|
|
162
|
+
return slotView.getBigInt64(0, true);
|
|
163
|
+
}
|
|
164
|
+
case TYPE_STRING_REF:
|
|
165
|
+
return { offset: slot.a, length: slot.b };
|
|
166
|
+
case TYPE_ARRAY_REF:
|
|
167
|
+
return { offset: slot.a, count: slot.b, capacity: slot.c };
|
|
168
|
+
default:
|
|
169
|
+
throw new Error(`Unknown slot type ${slot.type}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface SharedMemoryOptions {
|
|
174
|
+
initialSize?: number;
|
|
175
|
+
maxByteLength?: number;
|
|
176
|
+
/** Total memory budget shared across all buffers (see below). */
|
|
177
|
+
maxTotalBytes?: number;
|
|
178
|
+
lock?: Mutex;
|
|
179
|
+
internCapacity?: number;
|
|
180
|
+
internMax?: number;
|
|
181
|
+
buffers?: {
|
|
182
|
+
indexTable?: SharedArrayBuffer;
|
|
183
|
+
dataTable?: SharedArrayBuffer;
|
|
184
|
+
stringTable?: SharedArrayBuffer;
|
|
185
|
+
arrayTable?: SharedArrayBuffer;
|
|
186
|
+
layoutVersion?: SharedArrayBuffer;
|
|
187
|
+
refCount?: SharedArrayBuffer;
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Manages the four shared buffers and provides operations to store and read
|
|
193
|
+
* strings, arrays, and objects.
|
|
194
|
+
*/
|
|
195
|
+
export class SharedMemory {
|
|
196
|
+
readonly indexTable: GrowableBuffer;
|
|
197
|
+
readonly dataTable: GrowableBuffer;
|
|
198
|
+
readonly stringTable: GrowableBuffer;
|
|
199
|
+
readonly arrayTable: GrowableBuffer;
|
|
200
|
+
/**
|
|
201
|
+
* Cached Int32Array view over the Index Table, used to read/increment each
|
|
202
|
+
* key's version cell (stored as the 4th field of each index entry). Rebuilt
|
|
203
|
+
* on growth. The version is incremented on every write so workers can
|
|
204
|
+
* invalidate their read caches cross-worker.
|
|
205
|
+
*/
|
|
206
|
+
private versionView: Int32Array;
|
|
207
|
+
/**
|
|
208
|
+
* Global layout version cell. Bumped by the main thread after a
|
|
209
|
+
* defragmentation pass so workers can invalidate cached offsets (indexCache,
|
|
210
|
+
* layoutCache, propCache, readCache, internCache).
|
|
211
|
+
*/
|
|
212
|
+
readonly layoutVersion: Int32Array;
|
|
213
|
+
/** Cross-worker mutex protecting all read/write operations. */
|
|
214
|
+
readonly lock: Mutex;
|
|
215
|
+
/**
|
|
216
|
+
* The 5th shared buffer: reference-counting ring buffer + heartbeat +
|
|
217
|
+
* global refcount table. Shared with workers so they can report acquire/
|
|
218
|
+
* release events to the main thread's coordinator. Optional; created lazily
|
|
219
|
+
* by the coordinator when auto-GC is enabled.
|
|
220
|
+
*/
|
|
221
|
+
readonly refCountBuffer: SharedArrayBuffer | null;
|
|
222
|
+
|
|
223
|
+
private readonly encoder = new TextEncoder();
|
|
224
|
+
private readonly decoder = new TextDecoder();
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* LRU cache mapping interned strings to their stable StringTable refs.
|
|
228
|
+
* Per-worker (each worker has its own module instance). Refs are stable
|
|
229
|
+
* because the StringTable is append-only, so a cached ref stays valid.
|
|
230
|
+
* Grows dynamically up to `internMax`; evicts least-recently-used entries
|
|
231
|
+
* beyond `internCapacity`.
|
|
232
|
+
*/
|
|
233
|
+
private readonly internCache = new Map<string, StringRef>();
|
|
234
|
+
private internCapacity: number;
|
|
235
|
+
private readonly internMax: number;
|
|
236
|
+
/** Fast path: the most recently interned string and its ref. */
|
|
237
|
+
private lastInterned: string | undefined;
|
|
238
|
+
private lastInternedRef: StringRef | undefined;
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Monotonic counter for stable object/array identity. Each object and array
|
|
242
|
+
* gets a unique id that is independent of its DataTable/ArrayTable offset
|
|
243
|
+
* (which changes during defragmentation). Used to key the refcount table.
|
|
244
|
+
*/
|
|
245
|
+
private nextObjectId = 1;
|
|
246
|
+
|
|
247
|
+
constructor(options?: SharedMemoryOptions) {
|
|
248
|
+
const initial = options?.initialSize ?? DEFAULT_INITIAL;
|
|
249
|
+
// A single SharedArrayBuffer can be at most 4 GiB. `maxTotalBytes` is a
|
|
250
|
+
// *total* budget shared across all buffers: each buffer's own maxByteLength
|
|
251
|
+
// is capped at min(4 GiB, maxTotalBytes), and the sum of all buffers'
|
|
252
|
+
// physical sizes is additionally capped at maxTotalBytes. Without
|
|
253
|
+
// maxTotalBytes, each buffer simply grows up to its own maxByteLength.
|
|
254
|
+
const maxTotal = options?.maxTotalBytes;
|
|
255
|
+
const perBufferMax = Math.min(
|
|
256
|
+
4 * 1024 * 1024 * 1024,
|
|
257
|
+
maxTotal ?? options?.maxByteLength ?? DEFAULT_MAX,
|
|
258
|
+
);
|
|
259
|
+
const budget =
|
|
260
|
+
maxTotal !== undefined ? new SharedBudget(maxTotal) : undefined;
|
|
261
|
+
const buffers = options?.buffers;
|
|
262
|
+
this.indexTable = new GrowableBuffer(
|
|
263
|
+
buffers?.indexTable ??
|
|
264
|
+
new SharedArrayBuffer(initial, { maxByteLength: perBufferMax }),
|
|
265
|
+
perBufferMax,
|
|
266
|
+
budget,
|
|
267
|
+
);
|
|
268
|
+
this.dataTable = new GrowableBuffer(
|
|
269
|
+
buffers?.dataTable ??
|
|
270
|
+
new SharedArrayBuffer(initial, { maxByteLength: perBufferMax }),
|
|
271
|
+
perBufferMax,
|
|
272
|
+
budget,
|
|
273
|
+
);
|
|
274
|
+
this.stringTable = new GrowableBuffer(
|
|
275
|
+
buffers?.stringTable ??
|
|
276
|
+
new SharedArrayBuffer(initial, { maxByteLength: perBufferMax }),
|
|
277
|
+
perBufferMax,
|
|
278
|
+
budget,
|
|
279
|
+
);
|
|
280
|
+
this.arrayTable = new GrowableBuffer(
|
|
281
|
+
buffers?.arrayTable ??
|
|
282
|
+
new SharedArrayBuffer(initial, { maxByteLength: perBufferMax }),
|
|
283
|
+
perBufferMax,
|
|
284
|
+
budget,
|
|
285
|
+
);
|
|
286
|
+
this.versionView = new Int32Array(this.indexTable.buffer);
|
|
287
|
+
this.layoutVersion = new Int32Array(
|
|
288
|
+
buffers?.layoutVersion ??
|
|
289
|
+
new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT),
|
|
290
|
+
);
|
|
291
|
+
this.lock = options?.lock ?? new Mutex();
|
|
292
|
+
this.refCountBuffer = buffers?.refCount ?? null;
|
|
293
|
+
this.internCapacity = options?.internCapacity ?? DEFAULT_INTERN_CAPACITY;
|
|
294
|
+
this.internMax = options?.internMax ?? DEFAULT_INTERN_MAX;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Run `fn` while holding the shared lock. */
|
|
298
|
+
withLock<T>(fn: () => T): T {
|
|
299
|
+
return this.lock.withLock(fn);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Attach the reference-counting buffer (5th SAB) to this memory so it is
|
|
304
|
+
* included in `exportBuffers()`. No-op if a buffer is already attached.
|
|
305
|
+
*/
|
|
306
|
+
attachRefCountBuffer(sab: SharedArrayBuffer): void {
|
|
307
|
+
if (this.refCountBuffer === null) {
|
|
308
|
+
(this as { refCountBuffer: SharedArrayBuffer | null }).refCountBuffer =
|
|
309
|
+
sab;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Export all shared buffers (including the lock) so they can be sent to a
|
|
315
|
+
* worker via `postMessage` and reconstructed there with `fromBuffers()`.
|
|
316
|
+
*/
|
|
317
|
+
exportBuffers(): {
|
|
318
|
+
indexTable: SharedArrayBuffer;
|
|
319
|
+
dataTable: SharedArrayBuffer;
|
|
320
|
+
stringTable: SharedArrayBuffer;
|
|
321
|
+
arrayTable: SharedArrayBuffer;
|
|
322
|
+
layoutVersion: SharedArrayBuffer;
|
|
323
|
+
lock: SharedArrayBuffer;
|
|
324
|
+
refCount: SharedArrayBuffer | null;
|
|
325
|
+
} {
|
|
326
|
+
return {
|
|
327
|
+
indexTable: this.indexTable.buffer,
|
|
328
|
+
dataTable: this.dataTable.buffer,
|
|
329
|
+
stringTable: this.stringTable.buffer,
|
|
330
|
+
arrayTable: this.arrayTable.buffer,
|
|
331
|
+
layoutVersion: this.layoutVersion.buffer as SharedArrayBuffer,
|
|
332
|
+
lock: this.lock.buffer as SharedArrayBuffer,
|
|
333
|
+
refCount: this.refCountBuffer,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Reconstruct a SharedMemory instance from buffers exported by
|
|
339
|
+
* `exportBuffers()` (e.g. received from the main thread in a worker).
|
|
340
|
+
*/
|
|
341
|
+
static fromBuffers(buffers: {
|
|
342
|
+
indexTable: SharedArrayBuffer;
|
|
343
|
+
dataTable: SharedArrayBuffer;
|
|
344
|
+
stringTable: SharedArrayBuffer;
|
|
345
|
+
arrayTable: SharedArrayBuffer;
|
|
346
|
+
layoutVersion: SharedArrayBuffer;
|
|
347
|
+
lock: SharedArrayBuffer;
|
|
348
|
+
refCount?: SharedArrayBuffer | null;
|
|
349
|
+
}): SharedMemory {
|
|
350
|
+
return new SharedMemory({
|
|
351
|
+
buffers: {
|
|
352
|
+
indexTable: buffers.indexTable,
|
|
353
|
+
dataTable: buffers.dataTable,
|
|
354
|
+
stringTable: buffers.stringTable,
|
|
355
|
+
arrayTable: buffers.arrayTable,
|
|
356
|
+
layoutVersion: buffers.layoutVersion,
|
|
357
|
+
refCount: buffers.refCount ?? undefined,
|
|
358
|
+
},
|
|
359
|
+
lock: new Mutex(buffers.lock),
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ---------------------------------------------------------------------------
|
|
364
|
+
// StringTable
|
|
365
|
+
// ---------------------------------------------------------------------------
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Append a string to the StringTable and return its reference.
|
|
369
|
+
*
|
|
370
|
+
* Interns the string in a per-worker LRU cache: repeated writes of the same
|
|
371
|
+
* string reuse the existing StringTable ref instead of re-encoding and
|
|
372
|
+
* re-copying the bytes. The cache grows dynamically up to `internMax` and
|
|
373
|
+
* evicts the least-recently-used entry once it exceeds `internCapacity`.
|
|
374
|
+
*/
|
|
375
|
+
storeString(value: string): StringRef {
|
|
376
|
+
// Fast path: the same string as the previous call reuses its ref without
|
|
377
|
+
// touching the LRU map (avoids the expensive delete+set on every hit).
|
|
378
|
+
if (value === this.lastInterned) {
|
|
379
|
+
return this.lastInternedRef!;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const cached = this.internCache.get(value);
|
|
383
|
+
if (cached !== undefined) {
|
|
384
|
+
// Refresh LRU position (Map preserves insertion order; re-insert moves
|
|
385
|
+
// the entry to the most-recently-used end).
|
|
386
|
+
this.internCache.delete(value);
|
|
387
|
+
this.internCache.set(value, cached);
|
|
388
|
+
this.lastInterned = value;
|
|
389
|
+
this.lastInternedRef = cached;
|
|
390
|
+
return cached;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const bytes = this.encoder.encode(value);
|
|
394
|
+
const offset = this.stringTable.byteLength;
|
|
395
|
+
this.stringTable.ensureCapacity(offset + bytes.byteLength);
|
|
396
|
+
const view = this.stringTable.getView();
|
|
397
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
398
|
+
view.setUint8(offset + i, bytes[i]!);
|
|
399
|
+
}
|
|
400
|
+
this.stringTable.advance(bytes.byteLength);
|
|
401
|
+
const ref: StringRef = { offset, length: bytes.byteLength };
|
|
402
|
+
|
|
403
|
+
// Insert into the cache, growing capacity up to `internMax` before evicting.
|
|
404
|
+
if (this.internCache.size >= this.internCapacity) {
|
|
405
|
+
if (this.internCapacity < this.internMax) {
|
|
406
|
+
this.internCapacity = Math.min(this.internCapacity * 2, this.internMax);
|
|
407
|
+
} else {
|
|
408
|
+
// Evict the least-recently-used entry (first in insertion order).
|
|
409
|
+
const oldest = this.internCache.keys().next().value;
|
|
410
|
+
if (oldest !== undefined) this.internCache.delete(oldest);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
this.internCache.set(value, ref);
|
|
414
|
+
this.lastInterned = value;
|
|
415
|
+
this.lastInternedRef = ref;
|
|
416
|
+
return ref;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Clear the per-worker string interning cache.
|
|
421
|
+
*
|
|
422
|
+
* Call this if the StringTable is ever compacted/defragmented (which would
|
|
423
|
+
* invalidate cached refs). The cache is repopulated lazily on the next
|
|
424
|
+
* `storeString` calls.
|
|
425
|
+
*/
|
|
426
|
+
clearInternCache(): void {
|
|
427
|
+
this.internCache.clear();
|
|
428
|
+
this.lastInterned = undefined;
|
|
429
|
+
this.lastInternedRef = undefined;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Read a string from the StringTable by reference. */
|
|
433
|
+
readString(ref: StringRef): string {
|
|
434
|
+
const view = this.stringTable.getView();
|
|
435
|
+
const bytes = new Uint8Array(
|
|
436
|
+
this.stringTable.buffer,
|
|
437
|
+
ref.offset,
|
|
438
|
+
ref.length,
|
|
439
|
+
);
|
|
440
|
+
return this.decoder.decode(bytes);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ---------------------------------------------------------------------------
|
|
444
|
+
// ArrayTable
|
|
445
|
+
// ---------------------------------------------------------------------------
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Append an array of value slots to the ArrayTable and return its reference.
|
|
449
|
+
*
|
|
450
|
+
* Each array block starts with a 4-byte objectId header (for refcounting),
|
|
451
|
+
* followed by `capacity` value slots. `capacity` is the number of slots
|
|
452
|
+
* reserved for the array (>= slots.length). The extra slots are padding so
|
|
453
|
+
* `push` can grow the array in place without overwriting the next array.
|
|
454
|
+
* When the padding is exhausted, the array must be moved (see `moveArray`).
|
|
455
|
+
*/
|
|
456
|
+
storeArray(slots: ValueSlot[], capacity?: number): ArrayRef {
|
|
457
|
+
const count = slots.length;
|
|
458
|
+
const cap = Math.max(capacity ?? 0, count, 1);
|
|
459
|
+
const offset = this.arrayTable.byteLength;
|
|
460
|
+
this.arrayTable.ensureCapacity(offset + ARRAY_HEADER + cap * SLOT_SIZE);
|
|
461
|
+
const view = this.arrayTable.getView();
|
|
462
|
+
const objectId = this.nextObjectId++;
|
|
463
|
+
view.setInt32(offset, objectId, true);
|
|
464
|
+
for (let i = 0; i < count; i++) {
|
|
465
|
+
writeSlot(view, offset + ARRAY_HEADER + i * SLOT_SIZE, slots[i]!);
|
|
466
|
+
}
|
|
467
|
+
this.arrayTable.advance(ARRAY_HEADER + cap * SLOT_SIZE);
|
|
468
|
+
return { offset, count, capacity: cap };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** Read an array of value slots from the ArrayTable by reference. */
|
|
472
|
+
readArray(ref: ArrayRef): ValueSlot[] {
|
|
473
|
+
const view = this.arrayTable.getView();
|
|
474
|
+
const slots: ValueSlot[] = [];
|
|
475
|
+
for (let i = 0; i < ref.count; i++) {
|
|
476
|
+
slots.push(readSlot(view, ref.offset + ARRAY_HEADER + i * SLOT_SIZE));
|
|
477
|
+
}
|
|
478
|
+
return slots;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** Update a single value slot of an array in the ArrayTable in place. */
|
|
482
|
+
updateArraySlot(ref: ArrayRef, index: number, slot: ValueSlot): void {
|
|
483
|
+
writeSlot(
|
|
484
|
+
this.arrayTable.getView(),
|
|
485
|
+
ref.offset + ARRAY_HEADER + index * SLOT_SIZE,
|
|
486
|
+
slot,
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** Read a single value slot of an array from the ArrayTable. */
|
|
491
|
+
readArraySlot(ref: ArrayRef, index: number): ValueSlot {
|
|
492
|
+
return readSlot(
|
|
493
|
+
this.arrayTable.getView(),
|
|
494
|
+
ref.offset + ARRAY_HEADER + index * SLOT_SIZE,
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** Read the stable objectId of an array block in the ArrayTable. */
|
|
499
|
+
getArrayObjectId(offset: number): number {
|
|
500
|
+
return this.arrayTable.getView().getInt32(offset, true);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Move an array to a new location with a larger capacity, copying its slots.
|
|
505
|
+
* Returns the new reference. The caller must update the array-ref slot in
|
|
506
|
+
* the DataTable to point at the new location.
|
|
507
|
+
*/
|
|
508
|
+
moveArray(ref: ArrayRef, newCapacity: number): ArrayRef {
|
|
509
|
+
const newOffset = this.arrayTable.byteLength;
|
|
510
|
+
this.arrayTable.ensureCapacity(
|
|
511
|
+
newOffset + ARRAY_HEADER + newCapacity * SLOT_SIZE,
|
|
512
|
+
);
|
|
513
|
+
const view = this.arrayTable.getView();
|
|
514
|
+
const objectId = this.getArrayObjectId(ref.offset);
|
|
515
|
+
view.setInt32(newOffset, objectId, true);
|
|
516
|
+
for (let i = 0; i < ref.count; i++) {
|
|
517
|
+
writeSlot(
|
|
518
|
+
view,
|
|
519
|
+
newOffset + ARRAY_HEADER + i * SLOT_SIZE,
|
|
520
|
+
readSlot(view, ref.offset + ARRAY_HEADER + i * SLOT_SIZE),
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
this.arrayTable.advance(ARRAY_HEADER + newCapacity * SLOT_SIZE);
|
|
524
|
+
return { offset: newOffset, count: ref.count, capacity: newCapacity };
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Append a slot to an array, growing it in place if capacity allows, or
|
|
529
|
+
* moving it to a new location with doubled capacity otherwise. Returns the
|
|
530
|
+
* (possibly new) reference; the caller must update the array-ref slot in the
|
|
531
|
+
* DataTable if the reference changed.
|
|
532
|
+
*/
|
|
533
|
+
appendArraySlot(ref: ArrayRef, slot: ValueSlot): ArrayRef {
|
|
534
|
+
if (ref.count < ref.capacity) {
|
|
535
|
+
writeSlot(
|
|
536
|
+
this.arrayTable.getView(),
|
|
537
|
+
ref.offset + ARRAY_HEADER + ref.count * SLOT_SIZE,
|
|
538
|
+
slot,
|
|
539
|
+
);
|
|
540
|
+
return {
|
|
541
|
+
offset: ref.offset,
|
|
542
|
+
count: ref.count + 1,
|
|
543
|
+
capacity: ref.capacity,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
const moved = this.moveArray(ref, Math.max(ref.capacity * 2, 1));
|
|
547
|
+
writeSlot(
|
|
548
|
+
this.arrayTable.getView(),
|
|
549
|
+
moved.offset + ARRAY_HEADER + moved.count * SLOT_SIZE,
|
|
550
|
+
slot,
|
|
551
|
+
);
|
|
552
|
+
return {
|
|
553
|
+
offset: moved.offset,
|
|
554
|
+
count: moved.count + 1,
|
|
555
|
+
capacity: moved.capacity,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// ---------------------------------------------------------------------------
|
|
560
|
+
// DataTable
|
|
561
|
+
// ---------------------------------------------------------------------------
|
|
562
|
+
|
|
563
|
+
/** Store a single value slot in the DataTable and return its offset. */
|
|
564
|
+
storeValueSlot(slot: ValueSlot): number {
|
|
565
|
+
const offset = this.dataTable.byteLength;
|
|
566
|
+
this.dataTable.ensureCapacity(offset + SLOT_SIZE);
|
|
567
|
+
writeSlot(this.dataTable.getView(), offset, slot);
|
|
568
|
+
this.dataTable.advance(SLOT_SIZE);
|
|
569
|
+
return offset;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/** Read a single value slot from the DataTable. */
|
|
573
|
+
readValueSlot(offset: number): ValueSlot {
|
|
574
|
+
return readSlot(this.dataTable.getView(), offset);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** Update a single value slot in the DataTable. */
|
|
578
|
+
updateValueSlot(offset: number, slot: ValueSlot): void {
|
|
579
|
+
writeSlot(this.dataTable.getView(), offset, slot);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Create an object in the DataTable.
|
|
584
|
+
*
|
|
585
|
+
* @param properties array of [name, valueSlot] pairs
|
|
586
|
+
* @returns the byte offset of the object block in the DataTable
|
|
587
|
+
*/
|
|
588
|
+
createObject(properties: Array<[string, ValueSlot]>): number {
|
|
589
|
+
const blockSize =
|
|
590
|
+
OBJECT_HEADER + properties.length * (NAME_REF_SIZE + SLOT_SIZE);
|
|
591
|
+
const offset = this.dataTable.byteLength;
|
|
592
|
+
this.dataTable.ensureCapacity(offset + blockSize);
|
|
593
|
+
const view = this.dataTable.getView();
|
|
594
|
+
|
|
595
|
+
const objectId = this.nextObjectId++;
|
|
596
|
+
view.setInt32(offset, TYPE_OBJECT, true);
|
|
597
|
+
view.setInt32(offset + 4, properties.length, true);
|
|
598
|
+
view.setInt32(offset + 8, objectId, true);
|
|
599
|
+
|
|
600
|
+
let cursor = offset + OBJECT_HEADER;
|
|
601
|
+
for (const [name, slot] of properties) {
|
|
602
|
+
const nameRef = this.storeString(name);
|
|
603
|
+
view.setInt32(cursor, nameRef.offset, true);
|
|
604
|
+
view.setInt32(cursor + 4, nameRef.length, true);
|
|
605
|
+
cursor += NAME_REF_SIZE;
|
|
606
|
+
writeSlot(view, cursor, slot);
|
|
607
|
+
cursor += SLOT_SIZE;
|
|
608
|
+
}
|
|
609
|
+
this.dataTable.advance(blockSize);
|
|
610
|
+
return offset;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** Read an object's properties from the DataTable. */
|
|
614
|
+
readObject(offset: number): Array<[string, ValueSlot]> {
|
|
615
|
+
const view = this.dataTable.getView();
|
|
616
|
+
const count = view.getInt32(offset + 4, true);
|
|
617
|
+
const properties: Array<[string, ValueSlot]> = [];
|
|
618
|
+
let cursor = offset + OBJECT_HEADER;
|
|
619
|
+
for (let i = 0; i < count; i++) {
|
|
620
|
+
const nameOffset = view.getInt32(cursor, true);
|
|
621
|
+
const nameLength = view.getInt32(cursor + 4, true);
|
|
622
|
+
const name = this.readString({ offset: nameOffset, length: nameLength });
|
|
623
|
+
cursor += NAME_REF_SIZE;
|
|
624
|
+
const slot = readSlot(view, cursor);
|
|
625
|
+
cursor += SLOT_SIZE;
|
|
626
|
+
properties.push([name, slot]);
|
|
627
|
+
}
|
|
628
|
+
return properties;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/** Read the stable objectId of an object block in the DataTable. */
|
|
632
|
+
getObjectId(offset: number): number {
|
|
633
|
+
return this.dataTable.getView().getInt32(offset + 8, true);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Get the stable layout of an object value: its total DataTable offset and
|
|
638
|
+
* the slot offset of each property.
|
|
639
|
+
*
|
|
640
|
+
* Because object properties may not be added or removed, the layout is
|
|
641
|
+
* static for the lifetime of the object (it only changes if the object is
|
|
642
|
+
* moved by a future GC/defragmentation). Callers can cache it and read
|
|
643
|
+
* individual property slots directly without decoding the whole object.
|
|
644
|
+
*
|
|
645
|
+
* Returns null if the key does not exist or is not an object.
|
|
646
|
+
*/
|
|
647
|
+
getObjectLayout(key: string): ObjectLayout | null {
|
|
648
|
+
const offset = this.lookup(key);
|
|
649
|
+
if (offset === null) return null;
|
|
650
|
+
const view = this.dataTable.getView();
|
|
651
|
+
if (view.getInt32(offset, true) !== TYPE_OBJECT) return null;
|
|
652
|
+
const count = view.getInt32(offset + 4, true);
|
|
653
|
+
const properties = new Map<string, number>();
|
|
654
|
+
let cursor = offset + OBJECT_HEADER;
|
|
655
|
+
for (let i = 0; i < count; i++) {
|
|
656
|
+
const nameOffset = view.getInt32(cursor, true);
|
|
657
|
+
const nameLength = view.getInt32(cursor + 4, true);
|
|
658
|
+
const name = this.readString({ offset: nameOffset, length: nameLength });
|
|
659
|
+
cursor += NAME_REF_SIZE;
|
|
660
|
+
properties.set(name, cursor);
|
|
661
|
+
cursor += SLOT_SIZE;
|
|
662
|
+
}
|
|
663
|
+
return { totalOffset: offset, properties };
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/** Update a single value slot of an object in the DataTable. */
|
|
667
|
+
updateObjectSlot(
|
|
668
|
+
offset: number,
|
|
669
|
+
propertyIndex: number,
|
|
670
|
+
slot: ValueSlot,
|
|
671
|
+
): void {
|
|
672
|
+
const view = this.dataTable.getView();
|
|
673
|
+
const slotOffset =
|
|
674
|
+
offset +
|
|
675
|
+
OBJECT_HEADER +
|
|
676
|
+
propertyIndex * (NAME_REF_SIZE + SLOT_SIZE) +
|
|
677
|
+
NAME_REF_SIZE;
|
|
678
|
+
writeSlot(view, slotOffset, slot);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// ---------------------------------------------------------------------------
|
|
682
|
+
// Index Table
|
|
683
|
+
// ---------------------------------------------------------------------------
|
|
684
|
+
|
|
685
|
+
/** Look up a key in the Index Table, returning the DataTable offset or null. */
|
|
686
|
+
lookup(key: string): number | null {
|
|
687
|
+
const index = this.lookupIndex(key);
|
|
688
|
+
if (index === null) return null;
|
|
689
|
+
return this.lookupByIndex(index);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/** Read the DataTable offset for a known stable index, or null if absent. */
|
|
693
|
+
lookupByIndex(index: number): number | null {
|
|
694
|
+
const view = this.indexTable.getView();
|
|
695
|
+
const count = view.getInt32(0, true);
|
|
696
|
+
if (index >= count) return null;
|
|
697
|
+
const entryOffset = INDEX_HEADER + index * INDEX_ENTRY_SIZE;
|
|
698
|
+
return view.getInt32(entryOffset + 8, true);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Look up a key's stable index in the Index Table, or null if absent.
|
|
703
|
+
*
|
|
704
|
+
* The index is the key's position in the Index Table. Keys are never
|
|
705
|
+
* removed, so this index is stable for the lifetime of the key and can be
|
|
706
|
+
* used to address the key's version cell.
|
|
707
|
+
*/
|
|
708
|
+
lookupIndex(key: string): number | null {
|
|
709
|
+
const view = this.indexTable.getView();
|
|
710
|
+
const count = view.getInt32(0, true);
|
|
711
|
+
for (let i = 0; i < count; i++) {
|
|
712
|
+
const entryOffset = INDEX_HEADER + i * INDEX_ENTRY_SIZE;
|
|
713
|
+
const keyOffset = view.getInt32(entryOffset, true);
|
|
714
|
+
const keyLength = view.getInt32(entryOffset + 4, true);
|
|
715
|
+
const storedKey = this.readString({
|
|
716
|
+
offset: keyOffset,
|
|
717
|
+
length: keyLength,
|
|
718
|
+
});
|
|
719
|
+
if (storedKey === key) {
|
|
720
|
+
return i;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/** Read the current version for a key index (lock-free). */
|
|
727
|
+
getVersion(index: number): number {
|
|
728
|
+
return Atomics.load(this.versionView, versionCellIndex(index));
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/** Increment the version for a key index, invalidating read caches. */
|
|
732
|
+
bumpVersion(index: number): void {
|
|
733
|
+
Atomics.add(this.versionView, versionCellIndex(index), 1);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/** Insert a key → dataOffset mapping into the Index Table. */
|
|
737
|
+
insert(key: string, dataOffset: number): void {
|
|
738
|
+
const keyRef = this.storeString(key);
|
|
739
|
+
const view = this.indexTable.getView();
|
|
740
|
+
const count = view.getInt32(0, true);
|
|
741
|
+
const entryOffset = INDEX_HEADER + count * INDEX_ENTRY_SIZE;
|
|
742
|
+
this.indexTable.ensureCapacity(entryOffset + INDEX_ENTRY_SIZE);
|
|
743
|
+
view.setInt32(entryOffset, keyRef.offset, true);
|
|
744
|
+
view.setInt32(entryOffset + 4, keyRef.length, true);
|
|
745
|
+
view.setInt32(entryOffset + 8, dataOffset, true);
|
|
746
|
+
// The 4th field is the key's version cell, initialized to 0.
|
|
747
|
+
view.setInt32(entryOffset + INDEX_VERSION_OFFSET, 0, true);
|
|
748
|
+
view.setInt32(0, count + 1, true);
|
|
749
|
+
this.indexTable.advance(INDEX_ENTRY_SIZE);
|
|
750
|
+
|
|
751
|
+
// Rebuild the version view if the buffer grew (grow preserves the SAB in
|
|
752
|
+
// place, but the Int32Array view must be recreated over the new buffer).
|
|
753
|
+
this.versionView = new Int32Array(this.indexTable.buffer);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// ---------------------------------------------------------------------------
|
|
757
|
+
// Layout version (cache invalidation)
|
|
758
|
+
// ---------------------------------------------------------------------------
|
|
759
|
+
|
|
760
|
+
/** Read the current global layout version (lock-free). */
|
|
761
|
+
getLayoutVersion(): number {
|
|
762
|
+
return Atomics.load(this.layoutVersion, 0);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/** Bump the global layout version, invalidating all per-worker caches. */
|
|
766
|
+
bumpLayoutVersion(): void {
|
|
767
|
+
Atomics.add(this.layoutVersion, 0, 1);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// ---------------------------------------------------------------------------
|
|
771
|
+
// Defragmentation
|
|
772
|
+
// ---------------------------------------------------------------------------
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Compact the DataTable, ArrayTable, and StringTable, reclaiming dead space.
|
|
776
|
+
*
|
|
777
|
+
* Live data is everything reachable from the Index Table (every key's value,
|
|
778
|
+
* its object properties, and the strings/arrays they reference). Dead space
|
|
779
|
+
* (overwritten strings, abandoned array blocks from `moveArray`, and
|
|
780
|
+
* abandoned object blocks) is reclaimed by moving live data to the front of
|
|
781
|
+
* each buffer and resetting the used length.
|
|
782
|
+
*
|
|
783
|
+
* All references (Index Table offsets, object property names, string slots,
|
|
784
|
+
* array slots) are remapped in a single pass. The global layout version is
|
|
785
|
+
* bumped at the end so workers invalidate their cached offsets.
|
|
786
|
+
*
|
|
787
|
+
* Must be called under the shared lock (or on a single thread).
|
|
788
|
+
*/
|
|
789
|
+
defragment(): void {
|
|
790
|
+
const stringRemap = new Map<number, number>();
|
|
791
|
+
const arrayRemap = new Map<number, number>();
|
|
792
|
+
const objectRemap = new Map<number, number>();
|
|
793
|
+
const liveStrings = new Set<number>();
|
|
794
|
+
const liveArrays = new Set<number>();
|
|
795
|
+
const liveObjects = new Set<number>();
|
|
796
|
+
const liveDataOffsets = new Set<number>();
|
|
797
|
+
|
|
798
|
+
const markString = (offset: number) => liveStrings.add(offset);
|
|
799
|
+
|
|
800
|
+
const markValueSlot = (slot: ValueSlot) => {
|
|
801
|
+
switch (slot.type) {
|
|
802
|
+
case TYPE_STRING_REF:
|
|
803
|
+
case TYPE_BIGINT_STR:
|
|
804
|
+
liveStrings.add(slot.a);
|
|
805
|
+
break;
|
|
806
|
+
case TYPE_ARRAY_REF:
|
|
807
|
+
markArray(slot.a, slot.b);
|
|
808
|
+
break;
|
|
809
|
+
}
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
const markArray = (offset: number, count: number) => {
|
|
813
|
+
if (liveArrays.has(offset)) return;
|
|
814
|
+
liveArrays.add(offset);
|
|
815
|
+
const view = this.arrayTable.getView();
|
|
816
|
+
for (let i = 0; i < count; i++) {
|
|
817
|
+
markValueSlot(readSlot(view, offset + ARRAY_HEADER + i * SLOT_SIZE));
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
|
|
821
|
+
const markObject = (offset: number) => {
|
|
822
|
+
if (liveObjects.has(offset)) return;
|
|
823
|
+
liveObjects.add(offset);
|
|
824
|
+
liveDataOffsets.add(offset);
|
|
825
|
+
const view = this.dataTable.getView();
|
|
826
|
+
const count = view.getInt32(offset + 4, true);
|
|
827
|
+
let cursor = offset + OBJECT_HEADER;
|
|
828
|
+
for (let i = 0; i < count; i++) {
|
|
829
|
+
const nameOffset = view.getInt32(cursor, true);
|
|
830
|
+
const nameLength = view.getInt32(cursor + 4, true);
|
|
831
|
+
liveStrings.add(nameOffset);
|
|
832
|
+
cursor += NAME_REF_SIZE;
|
|
833
|
+
markValueSlot(readSlot(view, cursor));
|
|
834
|
+
cursor += SLOT_SIZE;
|
|
835
|
+
}
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
const markValue = (dataOffset: number) => {
|
|
839
|
+
const view = this.dataTable.getView();
|
|
840
|
+
if (view.getInt32(dataOffset, true) === TYPE_OBJECT) {
|
|
841
|
+
markObject(dataOffset);
|
|
842
|
+
} else {
|
|
843
|
+
liveDataOffsets.add(dataOffset);
|
|
844
|
+
markValueSlot(readSlot(view, dataOffset));
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
// Phase 1: Mark live data by walking the Index Table.
|
|
849
|
+
const indexView = this.indexTable.getView();
|
|
850
|
+
const indexCount = indexView.getInt32(0, true);
|
|
851
|
+
const indexEntries: Array<{ keyRef: StringRef; dataOffset: number }> = [];
|
|
852
|
+
for (let i = 0; i < indexCount; i++) {
|
|
853
|
+
const entryOffset = INDEX_HEADER + i * INDEX_ENTRY_SIZE;
|
|
854
|
+
const keyRef = {
|
|
855
|
+
offset: indexView.getInt32(entryOffset, true),
|
|
856
|
+
length: indexView.getInt32(entryOffset + 4, true),
|
|
857
|
+
};
|
|
858
|
+
const dataOffset = indexView.getInt32(entryOffset + 8, true);
|
|
859
|
+
indexEntries.push({ keyRef, dataOffset });
|
|
860
|
+
liveStrings.add(keyRef.offset);
|
|
861
|
+
markValue(dataOffset);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Phase 2: Compact the DataTable (objects and single value slots).
|
|
865
|
+
const sortedData = [...liveDataOffsets].sort((a, b) => a - b);
|
|
866
|
+
let newDataOffset = 0;
|
|
867
|
+
const dataView = this.dataTable.getView();
|
|
868
|
+
for (const oldOffset of sortedData) {
|
|
869
|
+
const size =
|
|
870
|
+
dataView.getInt32(oldOffset, true) === TYPE_OBJECT
|
|
871
|
+
? OBJECT_HEADER +
|
|
872
|
+
dataView.getInt32(oldOffset + 4, true) * (NAME_REF_SIZE + SLOT_SIZE)
|
|
873
|
+
: SLOT_SIZE;
|
|
874
|
+
copyBytes(dataView, newDataOffset, oldOffset, size);
|
|
875
|
+
objectRemap.set(oldOffset, newDataOffset);
|
|
876
|
+
newDataOffset += size;
|
|
877
|
+
}
|
|
878
|
+
this.dataTable.reset(newDataOffset);
|
|
879
|
+
|
|
880
|
+
// Phase 3: Compact the ArrayTable.
|
|
881
|
+
const sortedArrays = [...liveArrays].sort((a, b) => a - b);
|
|
882
|
+
let newArrayOffset = 0;
|
|
883
|
+
const arrayView = this.arrayTable.getView();
|
|
884
|
+
for (const oldOffset of sortedArrays) {
|
|
885
|
+
// Determine the array's capacity by scanning forward to the next live
|
|
886
|
+
// array or the end of the used region.
|
|
887
|
+
const next = sortedArrays[sortedArrays.indexOf(oldOffset) + 1];
|
|
888
|
+
const end = next !== undefined ? next : this.arrayTable.byteLength;
|
|
889
|
+
const capacity = (end - oldOffset - ARRAY_HEADER) / SLOT_SIZE;
|
|
890
|
+
copyBytes(
|
|
891
|
+
arrayView,
|
|
892
|
+
newArrayOffset,
|
|
893
|
+
oldOffset,
|
|
894
|
+
ARRAY_HEADER + capacity * SLOT_SIZE,
|
|
895
|
+
);
|
|
896
|
+
arrayRemap.set(oldOffset, newArrayOffset);
|
|
897
|
+
newArrayOffset += ARRAY_HEADER + capacity * SLOT_SIZE;
|
|
898
|
+
}
|
|
899
|
+
this.arrayTable.reset(newArrayOffset);
|
|
900
|
+
|
|
901
|
+
// Phase 4: Compact the StringTable.
|
|
902
|
+
const sortedStrings = [...liveStrings].sort((a, b) => a - b);
|
|
903
|
+
let newStringOffset = 0;
|
|
904
|
+
const stringView = this.stringTable.getView();
|
|
905
|
+
for (const oldOffset of sortedStrings) {
|
|
906
|
+
const next = sortedStrings[sortedStrings.indexOf(oldOffset) + 1];
|
|
907
|
+
const end = next !== undefined ? next : this.stringTable.byteLength;
|
|
908
|
+
const length = end - oldOffset;
|
|
909
|
+
copyBytes(stringView, newStringOffset, oldOffset, length);
|
|
910
|
+
stringRemap.set(oldOffset, newStringOffset);
|
|
911
|
+
newStringOffset += length;
|
|
912
|
+
}
|
|
913
|
+
this.stringTable.reset(newStringOffset);
|
|
914
|
+
|
|
915
|
+
// Phase 5: Rewrite the Index Table with remapped key strings and offsets.
|
|
916
|
+
for (let i = 0; i < indexCount; i++) {
|
|
917
|
+
const entryOffset = INDEX_HEADER + i * INDEX_ENTRY_SIZE;
|
|
918
|
+
const { keyRef, dataOffset } = indexEntries[i]!;
|
|
919
|
+
indexView.setInt32(entryOffset, stringRemap.get(keyRef.offset)!, true);
|
|
920
|
+
indexView.setInt32(entryOffset + 4, keyRef.length, true);
|
|
921
|
+
indexView.setInt32(entryOffset + 8, objectRemap.get(dataOffset)!, true);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// Phase 6: Rewrite object property names and value slots, and array slots.
|
|
925
|
+
for (const oldOffset of sortedData) {
|
|
926
|
+
if (dataView.getInt32(oldOffset, true) !== TYPE_OBJECT) continue;
|
|
927
|
+
const newOff = objectRemap.get(oldOffset)!;
|
|
928
|
+
const count = dataView.getInt32(newOff + 4, true);
|
|
929
|
+
let cursor = newOff + OBJECT_HEADER;
|
|
930
|
+
for (let i = 0; i < count; i++) {
|
|
931
|
+
const nameOffset = dataView.getInt32(cursor, true);
|
|
932
|
+
dataView.setInt32(cursor, stringRemap.get(nameOffset)!, true);
|
|
933
|
+
cursor += NAME_REF_SIZE;
|
|
934
|
+
remapSlot(dataView, cursor, stringRemap, arrayRemap);
|
|
935
|
+
cursor += SLOT_SIZE;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
for (const oldOffset of sortedData) {
|
|
939
|
+
if (dataView.getInt32(oldOffset, true) === TYPE_OBJECT) continue;
|
|
940
|
+
const newOff = objectRemap.get(oldOffset)!;
|
|
941
|
+
remapSlot(dataView, newOff, stringRemap, arrayRemap);
|
|
942
|
+
}
|
|
943
|
+
for (const oldOffset of sortedArrays) {
|
|
944
|
+
const newOff = arrayRemap.get(oldOffset)!;
|
|
945
|
+
const next = sortedArrays[sortedArrays.indexOf(oldOffset) + 1];
|
|
946
|
+
const end = next !== undefined ? next : this.arrayTable.byteLength;
|
|
947
|
+
const capacity = (end - oldOffset - ARRAY_HEADER) / SLOT_SIZE;
|
|
948
|
+
for (let i = 0; i < capacity; i++) {
|
|
949
|
+
remapSlot(
|
|
950
|
+
arrayView,
|
|
951
|
+
newOff + ARRAY_HEADER + i * SLOT_SIZE,
|
|
952
|
+
stringRemap,
|
|
953
|
+
arrayRemap,
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// Phase 7: Invalidate all per-worker caches.
|
|
959
|
+
this.bumpLayoutVersion();
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function writeSlot(view: DataView, offset: number, slot: ValueSlot): void {
|
|
964
|
+
view.setInt32(offset, slot.type, true);
|
|
965
|
+
view.setInt32(offset + 4, slot.a, true);
|
|
966
|
+
view.setInt32(offset + 8, slot.b, true);
|
|
967
|
+
view.setInt32(offset + 12, slot.c, true);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function readSlot(view: DataView, offset: number): ValueSlot {
|
|
971
|
+
return {
|
|
972
|
+
type: view.getInt32(offset, true),
|
|
973
|
+
a: view.getInt32(offset + 4, true),
|
|
974
|
+
b: view.getInt32(offset + 8, true),
|
|
975
|
+
c: view.getInt32(offset + 12, true),
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/** Copy `len` bytes from `src` to `dst` in the same buffer (dst <= src). */
|
|
980
|
+
function copyBytes(
|
|
981
|
+
view: DataView,
|
|
982
|
+
dst: number,
|
|
983
|
+
src: number,
|
|
984
|
+
len: number,
|
|
985
|
+
): void {
|
|
986
|
+
for (let i = 0; i < len; i++) {
|
|
987
|
+
view.setUint8(dst + i, view.getUint8(src + i));
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* Rewrite a value slot's string/array references after compaction, in place.
|
|
993
|
+
* String refs and bigint-str refs are remapped via `stringRemap`; array refs
|
|
994
|
+
* via `arrayRemap`.
|
|
995
|
+
*/
|
|
996
|
+
function remapSlot(
|
|
997
|
+
view: DataView,
|
|
998
|
+
offset: number,
|
|
999
|
+
stringRemap: Map<number, number>,
|
|
1000
|
+
arrayRemap: Map<number, number>,
|
|
1001
|
+
): void {
|
|
1002
|
+
const type = view.getInt32(offset, true);
|
|
1003
|
+
if (type === TYPE_STRING_REF || type === TYPE_BIGINT_STR) {
|
|
1004
|
+
view.setInt32(
|
|
1005
|
+
offset + 4,
|
|
1006
|
+
stringRemap.get(view.getInt32(offset + 4, true))!,
|
|
1007
|
+
true,
|
|
1008
|
+
);
|
|
1009
|
+
} else if (type === TYPE_ARRAY_REF) {
|
|
1010
|
+
view.setInt32(
|
|
1011
|
+
offset + 4,
|
|
1012
|
+
arrayRemap.get(view.getInt32(offset + 4, true))!,
|
|
1013
|
+
true,
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
}
|