bun-shared-memory 0.1.7 → 0.2.0
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 +76 -0
- package/README.md +18 -18
- package/package.json +2 -1
- package/src/growable-buffer.ts +198 -4
- package/src/memory.ts +404 -41
- package/src/refcount.ts +31 -8
- package/src/shared.ts +643 -168
package/src/memory.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
GrowableBuffer,
|
|
3
|
+
MIN_BLOCK_SIZE,
|
|
3
4
|
SharedBudget,
|
|
4
5
|
nextPowerOfTwo,
|
|
5
6
|
} from "./growable-buffer.ts";
|
|
@@ -48,6 +49,13 @@ export const TYPE_STRING_REF = 3;
|
|
|
48
49
|
export const TYPE_ARRAY_REF = 4;
|
|
49
50
|
export const TYPE_OBJECT = 5;
|
|
50
51
|
export const TYPE_BIGINT_STR = 6;
|
|
52
|
+
/** A string value stored in the ArrayTable (slab-allocated, refcounted). */
|
|
53
|
+
export const TYPE_STRING_VALUE_REF = 7;
|
|
54
|
+
/**
|
|
55
|
+
* A short string stored inline in the slot itself (up to 12 UTF-8 bytes in
|
|
56
|
+
* the a/b/c fields), avoiding a heap allocation and the string-table write.
|
|
57
|
+
*/
|
|
58
|
+
export const TYPE_INLINE_STRING = 8;
|
|
51
59
|
|
|
52
60
|
const SLOT_SIZE = 16;
|
|
53
61
|
const OBJECT_HEADER = 16;
|
|
@@ -149,6 +157,11 @@ export function stringRefSlot(ref: StringRef): ValueSlot {
|
|
|
149
157
|
return { type: TYPE_STRING_REF, a: ref.offset, b: ref.length, c: 0 };
|
|
150
158
|
}
|
|
151
159
|
|
|
160
|
+
/** Encode a string value (in the ArrayTable) into a ValueSlot. */
|
|
161
|
+
export function stringValueRefSlot(ref: StringRef): ValueSlot {
|
|
162
|
+
return { type: TYPE_STRING_VALUE_REF, a: ref.offset, b: ref.length, c: 0 };
|
|
163
|
+
}
|
|
164
|
+
|
|
152
165
|
/** Encode a bigint stored as a decimal string into a ValueSlot. */
|
|
153
166
|
export function bigintStrSlot(ref: StringRef): ValueSlot {
|
|
154
167
|
return { type: TYPE_BIGINT_STR, a: ref.offset, b: ref.length, c: 0 };
|
|
@@ -176,6 +189,8 @@ export function decodeSlot(slot: ValueSlot): unknown {
|
|
|
176
189
|
}
|
|
177
190
|
case TYPE_STRING_REF:
|
|
178
191
|
return { offset: slot.a, length: slot.b };
|
|
192
|
+
case TYPE_STRING_VALUE_REF:
|
|
193
|
+
return { offset: slot.a, length: slot.b };
|
|
179
194
|
case TYPE_ARRAY_REF:
|
|
180
195
|
return { offset: slot.a, count: slot.b, capacity: slot.c };
|
|
181
196
|
default:
|
|
@@ -211,6 +226,17 @@ export interface SharedMemoryOptions {
|
|
|
211
226
|
layoutVersion?: SharedArrayBuffer;
|
|
212
227
|
refCount?: SharedArrayBuffer;
|
|
213
228
|
budget?: SharedArrayBuffer;
|
|
229
|
+
cursors?: {
|
|
230
|
+
indexTable?: SharedArrayBuffer;
|
|
231
|
+
dataTable?: SharedArrayBuffer;
|
|
232
|
+
stringTable?: SharedArrayBuffer;
|
|
233
|
+
arrayTable?: SharedArrayBuffer;
|
|
234
|
+
};
|
|
235
|
+
freeLists?: {
|
|
236
|
+
dataTable?: SharedArrayBuffer;
|
|
237
|
+
stringTable?: SharedArrayBuffer;
|
|
238
|
+
arrayTable?: SharedArrayBuffer;
|
|
239
|
+
};
|
|
214
240
|
};
|
|
215
241
|
}
|
|
216
242
|
|
|
@@ -223,6 +249,8 @@ function makeBuffer(
|
|
|
223
249
|
chunkSize?: number,
|
|
224
250
|
singleBuffer = false,
|
|
225
251
|
chargeInitial = true,
|
|
252
|
+
cursorBuffer?: SharedArrayBuffer,
|
|
253
|
+
freeListBuffer?: SharedArrayBuffer,
|
|
226
254
|
): GrowableBuffer {
|
|
227
255
|
if (buf === undefined) {
|
|
228
256
|
return new GrowableBuffer(
|
|
@@ -232,6 +260,8 @@ function makeBuffer(
|
|
|
232
260
|
chunkSize,
|
|
233
261
|
singleBuffer,
|
|
234
262
|
chargeInitial,
|
|
263
|
+
cursorBuffer,
|
|
264
|
+
freeListBuffer,
|
|
235
265
|
);
|
|
236
266
|
}
|
|
237
267
|
return new GrowableBuffer(
|
|
@@ -241,6 +271,8 @@ function makeBuffer(
|
|
|
241
271
|
chunkSize,
|
|
242
272
|
singleBuffer,
|
|
243
273
|
chargeInitial,
|
|
274
|
+
cursorBuffer,
|
|
275
|
+
freeListBuffer,
|
|
244
276
|
);
|
|
245
277
|
}
|
|
246
278
|
|
|
@@ -265,7 +297,7 @@ export class SharedMemory {
|
|
|
265
297
|
* on growth. The version is incremented on every write so workers can
|
|
266
298
|
* invalidate their read caches cross-worker.
|
|
267
299
|
*/
|
|
268
|
-
|
|
300
|
+
versionView: Int32Array;
|
|
269
301
|
/**
|
|
270
302
|
* Global layout version cell. Bumped by the main thread after a
|
|
271
303
|
* defragmentation pass so workers can invalidate cached offsets (indexCache,
|
|
@@ -359,6 +391,8 @@ export class SharedMemory {
|
|
|
359
391
|
undefined,
|
|
360
392
|
chunkSize,
|
|
361
393
|
true,
|
|
394
|
+
true,
|
|
395
|
+
buffers?.cursors?.indexTable,
|
|
362
396
|
);
|
|
363
397
|
this.dataTable = makeBuffer(
|
|
364
398
|
buffers?.dataTable,
|
|
@@ -368,6 +402,8 @@ export class SharedMemory {
|
|
|
368
402
|
chunkSize,
|
|
369
403
|
false,
|
|
370
404
|
chargeInitial,
|
|
405
|
+
buffers?.cursors?.dataTable,
|
|
406
|
+
buffers?.freeLists?.dataTable,
|
|
371
407
|
);
|
|
372
408
|
this.stringTable = makeBuffer(
|
|
373
409
|
buffers?.stringTable,
|
|
@@ -377,6 +413,8 @@ export class SharedMemory {
|
|
|
377
413
|
chunkSize,
|
|
378
414
|
false,
|
|
379
415
|
chargeInitial,
|
|
416
|
+
buffers?.cursors?.stringTable,
|
|
417
|
+
buffers?.freeLists?.stringTable,
|
|
380
418
|
);
|
|
381
419
|
this.arrayTable = makeBuffer(
|
|
382
420
|
buffers?.arrayTable,
|
|
@@ -386,11 +424,13 @@ export class SharedMemory {
|
|
|
386
424
|
chunkSize,
|
|
387
425
|
false,
|
|
388
426
|
chargeInitial,
|
|
427
|
+
buffers?.cursors?.arrayTable,
|
|
428
|
+
buffers?.freeLists?.arrayTable,
|
|
389
429
|
);
|
|
390
430
|
this.versionView = new Int32Array(this.indexTable.buffers[0]!);
|
|
391
431
|
this.layoutVersion = new Int32Array(
|
|
392
432
|
buffers?.layoutVersion ??
|
|
393
|
-
new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT),
|
|
433
|
+
new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 3),
|
|
394
434
|
);
|
|
395
435
|
this.lock = options?.lock ?? new Mutex();
|
|
396
436
|
this.refCountBuffer = buffers?.refCount ?? null;
|
|
@@ -453,6 +493,17 @@ export class SharedMemory {
|
|
|
453
493
|
indexTableMaxByteLength: number;
|
|
454
494
|
maxTotalBytes: number | null;
|
|
455
495
|
budget: SharedArrayBuffer | null;
|
|
496
|
+
cursors: {
|
|
497
|
+
indexTable: SharedArrayBuffer;
|
|
498
|
+
dataTable: SharedArrayBuffer;
|
|
499
|
+
stringTable: SharedArrayBuffer;
|
|
500
|
+
arrayTable: SharedArrayBuffer;
|
|
501
|
+
};
|
|
502
|
+
freeLists: {
|
|
503
|
+
dataTable: SharedArrayBuffer;
|
|
504
|
+
stringTable: SharedArrayBuffer;
|
|
505
|
+
arrayTable: SharedArrayBuffer;
|
|
506
|
+
};
|
|
456
507
|
} {
|
|
457
508
|
return {
|
|
458
509
|
indexTable: this.indexTable.buffers,
|
|
@@ -466,6 +517,17 @@ export class SharedMemory {
|
|
|
466
517
|
indexTableMaxByteLength: this.indexTableMaxByteLength,
|
|
467
518
|
maxTotalBytes: this.maxTotalBytes,
|
|
468
519
|
budget: this.budgetBuffer,
|
|
520
|
+
cursors: {
|
|
521
|
+
indexTable: this.indexTable.cursorBuffer,
|
|
522
|
+
dataTable: this.dataTable.cursorBuffer,
|
|
523
|
+
stringTable: this.stringTable.cursorBuffer,
|
|
524
|
+
arrayTable: this.arrayTable.cursorBuffer,
|
|
525
|
+
},
|
|
526
|
+
freeLists: {
|
|
527
|
+
dataTable: this.dataTable.freeListBuffer,
|
|
528
|
+
stringTable: this.stringTable.freeListBuffer,
|
|
529
|
+
arrayTable: this.arrayTable.freeListBuffer,
|
|
530
|
+
},
|
|
469
531
|
};
|
|
470
532
|
}
|
|
471
533
|
|
|
@@ -485,6 +547,17 @@ export class SharedMemory {
|
|
|
485
547
|
indexTableMaxByteLength?: number;
|
|
486
548
|
maxTotalBytes?: number | null;
|
|
487
549
|
budget?: SharedArrayBuffer | null;
|
|
550
|
+
cursors?: {
|
|
551
|
+
indexTable?: SharedArrayBuffer;
|
|
552
|
+
dataTable?: SharedArrayBuffer;
|
|
553
|
+
stringTable?: SharedArrayBuffer;
|
|
554
|
+
arrayTable?: SharedArrayBuffer;
|
|
555
|
+
};
|
|
556
|
+
freeLists?: {
|
|
557
|
+
dataTable?: SharedArrayBuffer;
|
|
558
|
+
stringTable?: SharedArrayBuffer;
|
|
559
|
+
arrayTable?: SharedArrayBuffer;
|
|
560
|
+
};
|
|
488
561
|
}): SharedMemory {
|
|
489
562
|
const indexBuffer = Array.isArray(buffers.indexTable)
|
|
490
563
|
? buffers.indexTable[0]
|
|
@@ -505,6 +578,8 @@ export class SharedMemory {
|
|
|
505
578
|
layoutVersion: buffers.layoutVersion,
|
|
506
579
|
refCount: buffers.refCount ?? undefined,
|
|
507
580
|
budget: buffers.budget ?? undefined,
|
|
581
|
+
cursors: buffers.cursors,
|
|
582
|
+
freeLists: buffers.freeLists,
|
|
508
583
|
},
|
|
509
584
|
lock: new Mutex(buffers.lock),
|
|
510
585
|
});
|
|
@@ -541,10 +616,8 @@ export class SharedMemory {
|
|
|
541
616
|
}
|
|
542
617
|
|
|
543
618
|
const bytes = this.encoder.encode(value);
|
|
544
|
-
const offset = this.stringTable.byteLength;
|
|
545
|
-
this.stringTable.ensureCapacity(offset + bytes.byteLength);
|
|
619
|
+
const offset = this.stringTable.reserve(bytes.byteLength);
|
|
546
620
|
this.stringTable.writeBytes(offset, bytes);
|
|
547
|
-
this.stringTable.advance(bytes.byteLength);
|
|
548
621
|
const ref: StringRef = { offset, length: bytes.byteLength };
|
|
549
622
|
|
|
550
623
|
// Insert into the cache, growing capacity up to `internMax` before evicting.
|
|
@@ -582,6 +655,86 @@ export class SharedMemory {
|
|
|
582
655
|
return this.decoder.decode(bytes);
|
|
583
656
|
}
|
|
584
657
|
|
|
658
|
+
/**
|
|
659
|
+
* Store a string value in the ArrayTable as a slab-allocated block.
|
|
660
|
+
*
|
|
661
|
+
* Unlike `storeString` (which interns into the append-only StringTable for
|
|
662
|
+
* property names and keys), string values are stored in the ArrayTable so
|
|
663
|
+
* they can be freed and reused when their reference is overwritten. Each
|
|
664
|
+
* block starts with a 4-byte objectId header (for refcounting), followed by
|
|
665
|
+
* the UTF-8 bytes of the string.
|
|
666
|
+
*
|
|
667
|
+
* @returns a StringRef pointing into the ArrayTable.
|
|
668
|
+
*/
|
|
669
|
+
storeStringValue(value: string, oldRef?: StringRef): StringRef {
|
|
670
|
+
const bytes = this.encoder.encode(value);
|
|
671
|
+
const size = ARRAY_HEADER + bytes.byteLength;
|
|
672
|
+
// In-place update: if the new string fits in the existing block (same
|
|
673
|
+
// length or shorter), overwrite it without allocating a new block. This
|
|
674
|
+
// keeps repeated writes to the same string property cheap and avoids
|
|
675
|
+
// unbounded ArrayTable growth. The block's objectId header is preserved.
|
|
676
|
+
if (oldRef !== undefined && size <= ARRAY_HEADER + oldRef.length) {
|
|
677
|
+
this.arrayTable.writeBytes(oldRef.offset + ARRAY_HEADER, bytes);
|
|
678
|
+
return { offset: oldRef.offset, length: bytes.byteLength };
|
|
679
|
+
}
|
|
680
|
+
const offset = this.arrayTable.reserve(size);
|
|
681
|
+
const objectId = this.nextObjectId++;
|
|
682
|
+
this.arrayTable.setInt32(offset, objectId);
|
|
683
|
+
this.arrayTable.writeBytes(offset + ARRAY_HEADER, bytes);
|
|
684
|
+
return { offset, length: bytes.byteLength };
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/** Read a string value from the ArrayTable by reference. */
|
|
688
|
+
readStringValue(ref: StringRef): string {
|
|
689
|
+
const bytes = this.arrayTable.readBytes(
|
|
690
|
+
ref.offset + ARRAY_HEADER,
|
|
691
|
+
ref.length,
|
|
692
|
+
);
|
|
693
|
+
return this.decoder.decode(bytes);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/** Read a short string stored inline in a DataTable slot. */
|
|
697
|
+
readInlineString(a: number, b: number, c: number): string {
|
|
698
|
+
const length = c >>> 28;
|
|
699
|
+
const bytes = new Uint8Array(length);
|
|
700
|
+
for (let i = 0; i < length; i++) {
|
|
701
|
+
const word = i < 4 ? a : i < 8 ? b : c;
|
|
702
|
+
bytes[i] = (word >>> ((i % 4) * 8)) & 0xff;
|
|
703
|
+
}
|
|
704
|
+
return this.decoder.decode(bytes);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Free a string value block in the ArrayTable, pushing it onto the free
|
|
709
|
+
* list so it can be reused. Detects double frees.
|
|
710
|
+
*
|
|
711
|
+
* No-op: string-value blocks are no longer freed on the hot path (they are
|
|
712
|
+
* reclaimed by defragmentation instead), so this is kept only for API
|
|
713
|
+
* compatibility.
|
|
714
|
+
*/
|
|
715
|
+
freeStringValue(_ref: StringRef): void {
|
|
716
|
+
// Intentionally a no-op; dead string blocks are reclaimed by defrag.
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Release a reference to a string value.
|
|
721
|
+
*
|
|
722
|
+
* No-op: string-value refcounting is disabled. Blocks are never freed on the
|
|
723
|
+
* hot path; dead space is reclaimed by defragmentation.
|
|
724
|
+
*/
|
|
725
|
+
releaseStringValue(_ref: StringRef): void {
|
|
726
|
+
// Intentionally a no-op; see freeStringValue.
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Acquire a reference to a string value.
|
|
731
|
+
*
|
|
732
|
+
* No-op: string-value refcounting is disabled.
|
|
733
|
+
*/
|
|
734
|
+
acquireStringValue(_ref: StringRef): void {
|
|
735
|
+
// Intentionally a no-op; see freeStringValue.
|
|
736
|
+
}
|
|
737
|
+
|
|
585
738
|
// ---------------------------------------------------------------------------
|
|
586
739
|
// ArrayTable
|
|
587
740
|
// ---------------------------------------------------------------------------
|
|
@@ -598,8 +751,8 @@ export class SharedMemory {
|
|
|
598
751
|
storeArray(slots: ValueSlot[], capacity?: number): ArrayRef {
|
|
599
752
|
const count = slots.length;
|
|
600
753
|
const cap = Math.max(capacity ?? 0, count, 1);
|
|
601
|
-
const
|
|
602
|
-
this.arrayTable.
|
|
754
|
+
const size = ARRAY_HEADER + cap * SLOT_SIZE;
|
|
755
|
+
const offset = this.arrayTable.reserve(size);
|
|
603
756
|
const objectId = this.nextObjectId++;
|
|
604
757
|
this.arrayTable.setInt32(offset, objectId);
|
|
605
758
|
for (let i = 0; i < count; i++) {
|
|
@@ -609,7 +762,6 @@ export class SharedMemory {
|
|
|
609
762
|
slots[i]!,
|
|
610
763
|
);
|
|
611
764
|
}
|
|
612
|
-
this.arrayTable.advance(ARRAY_HEADER + cap * SLOT_SIZE);
|
|
613
765
|
return { offset, count, capacity: cap };
|
|
614
766
|
}
|
|
615
767
|
|
|
@@ -633,6 +785,63 @@ export class SharedMemory {
|
|
|
633
785
|
);
|
|
634
786
|
}
|
|
635
787
|
|
|
788
|
+
/**
|
|
789
|
+
* Fast-path: write a float64 number into an array slot directly, avoiding
|
|
790
|
+
* the ValueSlot object allocation and the 4 separate Int32 writes.
|
|
791
|
+
*/
|
|
792
|
+
updateArrayNumberSlot(ref: ArrayRef, index: number, value: number): void {
|
|
793
|
+
slotView.setFloat64(0, value, true);
|
|
794
|
+
const buf = this.arrayTable;
|
|
795
|
+
const off = ref.offset + ARRAY_HEADER + index * SLOT_SIZE;
|
|
796
|
+
buf.setInt32(off, TYPE_NUMBER);
|
|
797
|
+
buf.setInt32(off + 4, slotView.getInt32(0, true));
|
|
798
|
+
buf.setInt32(off + 8, slotView.getInt32(4, true));
|
|
799
|
+
buf.setInt32(off + 12, 0);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Fast-path: write a boolean into an array slot directly, avoiding the
|
|
804
|
+
* ValueSlot object allocation and the 4 separate Int32 writes.
|
|
805
|
+
*/
|
|
806
|
+
updateArrayBooleanSlot(ref: ArrayRef, index: number, value: boolean): void {
|
|
807
|
+
const buf = this.arrayTable;
|
|
808
|
+
const off = ref.offset + ARRAY_HEADER + index * SLOT_SIZE;
|
|
809
|
+
buf.setInt32(off, TYPE_BOOLEAN);
|
|
810
|
+
buf.setInt32(off + 4, value ? 1 : 0);
|
|
811
|
+
buf.setInt32(off + 8, 0);
|
|
812
|
+
buf.setInt32(off + 12, 0);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Fast-path: write a short string inline into an array slot (up to 11
|
|
817
|
+
* UTF-8 bytes), avoiding a heap allocation and the string-table write.
|
|
818
|
+
* Returns true if the string fit inline; false if it must go to the heap.
|
|
819
|
+
*/
|
|
820
|
+
updateArrayInlineStringSlot(
|
|
821
|
+
ref: ArrayRef,
|
|
822
|
+
index: number,
|
|
823
|
+
value: string,
|
|
824
|
+
): boolean {
|
|
825
|
+
const bytes = this.encoder.encode(value);
|
|
826
|
+
if (bytes.byteLength > 11) return false;
|
|
827
|
+
const buf = this.arrayTable;
|
|
828
|
+
const off = ref.offset + ARRAY_HEADER + index * SLOT_SIZE;
|
|
829
|
+
buf.setInt32(off, TYPE_INLINE_STRING);
|
|
830
|
+
let a = 0;
|
|
831
|
+
let b = 0;
|
|
832
|
+
let c = bytes.byteLength << 28;
|
|
833
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
834
|
+
const byte = bytes[i]!;
|
|
835
|
+
if (i < 4) a |= byte << (i * 8);
|
|
836
|
+
else if (i < 8) b |= byte << ((i - 4) * 8);
|
|
837
|
+
else c |= byte << ((i - 8) * 8);
|
|
838
|
+
}
|
|
839
|
+
buf.setInt32(off + 4, a);
|
|
840
|
+
buf.setInt32(off + 8, b);
|
|
841
|
+
buf.setInt32(off + 12, c);
|
|
842
|
+
return true;
|
|
843
|
+
}
|
|
844
|
+
|
|
636
845
|
/** Read a single value slot of an array from the ArrayTable. */
|
|
637
846
|
readArraySlot(ref: ArrayRef, index: number): ValueSlot {
|
|
638
847
|
return readSlot(
|
|
@@ -652,10 +861,8 @@ export class SharedMemory {
|
|
|
652
861
|
* the DataTable to point at the new location.
|
|
653
862
|
*/
|
|
654
863
|
moveArray(ref: ArrayRef, newCapacity: number): ArrayRef {
|
|
655
|
-
const
|
|
656
|
-
this.arrayTable.
|
|
657
|
-
newOffset + ARRAY_HEADER + newCapacity * SLOT_SIZE,
|
|
658
|
-
);
|
|
864
|
+
const size = ARRAY_HEADER + newCapacity * SLOT_SIZE;
|
|
865
|
+
const newOffset = this.arrayTable.reserve(size);
|
|
659
866
|
const objectId = this.getArrayObjectId(ref.offset);
|
|
660
867
|
this.arrayTable.setInt32(newOffset, objectId);
|
|
661
868
|
for (let i = 0; i < ref.count; i++) {
|
|
@@ -665,7 +872,13 @@ export class SharedMemory {
|
|
|
665
872
|
readSlot(this.arrayTable, ref.offset + ARRAY_HEADER + i * SLOT_SIZE),
|
|
666
873
|
);
|
|
667
874
|
}
|
|
668
|
-
|
|
875
|
+
// The old block is no longer referenced; it is reclaimed by defrag.
|
|
876
|
+
this.freeArray(ref);
|
|
877
|
+
// The array moved to a new offset, so any cached ArrayRef (e.g. the
|
|
878
|
+
// per-target `__arrayRef` on the write hot path) is now stale. Bump the
|
|
879
|
+
// global layout version so those caches are invalidated, exactly as a
|
|
880
|
+
// defragmentation pass does.
|
|
881
|
+
this.bumpLayoutVersion();
|
|
669
882
|
return { offset: newOffset, count: ref.count, capacity: newCapacity };
|
|
670
883
|
}
|
|
671
884
|
|
|
@@ -701,16 +914,23 @@ export class SharedMemory {
|
|
|
701
914
|
};
|
|
702
915
|
}
|
|
703
916
|
|
|
917
|
+
/**
|
|
918
|
+
* Free an array block, pushing it onto the ArrayTable free list so it can
|
|
919
|
+
* be reused. Detects double frees. The block's data is overwritten with
|
|
920
|
+
* free-list metadata, so it must not be read after freeing.
|
|
921
|
+
*/
|
|
922
|
+
freeArray(_ref: ArrayRef): void {
|
|
923
|
+
// No-op: array blocks are reclaimed by defrag, not the free list.
|
|
924
|
+
}
|
|
925
|
+
|
|
704
926
|
// ---------------------------------------------------------------------------
|
|
705
927
|
// DataTable
|
|
706
928
|
// ---------------------------------------------------------------------------
|
|
707
929
|
|
|
708
930
|
/** Store a single value slot in the DataTable and return its offset. */
|
|
709
931
|
storeValueSlot(slot: ValueSlot): number {
|
|
710
|
-
const offset = this.dataTable.
|
|
711
|
-
this.dataTable.ensureCapacity(offset + SLOT_SIZE);
|
|
932
|
+
const offset = this.dataTable.reserve(SLOT_SIZE);
|
|
712
933
|
writeSlot(this.dataTable, offset, slot);
|
|
713
|
-
this.dataTable.advance(SLOT_SIZE);
|
|
714
934
|
return offset;
|
|
715
935
|
}
|
|
716
936
|
|
|
@@ -724,6 +944,62 @@ export class SharedMemory {
|
|
|
724
944
|
writeSlot(this.dataTable, offset, slot);
|
|
725
945
|
}
|
|
726
946
|
|
|
947
|
+
/**
|
|
948
|
+
* Fast-path: write a float64 number slot directly into the DataTable,
|
|
949
|
+
* avoiding the ValueSlot object allocation and the 4 separate Int32
|
|
950
|
+
* writes of the generic path.
|
|
951
|
+
*/
|
|
952
|
+
updateNumberSlot(offset: number, value: number): void {
|
|
953
|
+
slotView.setFloat64(0, value, true);
|
|
954
|
+
const buf = this.dataTable;
|
|
955
|
+
buf.setInt32(offset, TYPE_NUMBER);
|
|
956
|
+
buf.setInt32(offset + 4, slotView.getInt32(0, true));
|
|
957
|
+
buf.setInt32(offset + 8, slotView.getInt32(4, true));
|
|
958
|
+
buf.setInt32(offset + 12, 0);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* Fast-path: write a boolean slot directly into the DataTable, avoiding
|
|
963
|
+
* the ValueSlot object allocation of the generic path.
|
|
964
|
+
*/
|
|
965
|
+
updateBooleanSlot(offset: number, value: boolean): void {
|
|
966
|
+
const buf = this.dataTable;
|
|
967
|
+
buf.setInt32(offset, TYPE_BOOLEAN);
|
|
968
|
+
buf.setInt32(offset + 4, value ? 1 : 0);
|
|
969
|
+
buf.setInt32(offset + 8, 0);
|
|
970
|
+
buf.setInt32(offset + 12, 0);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Fast-path: write a short string inline into the slot itself (up to 11
|
|
975
|
+
* UTF-8 bytes), avoiding a heap allocation and the string-table write.
|
|
976
|
+
* The bytes are packed into the slot's a/b/c fields (11 bytes) with the
|
|
977
|
+
* length in the top 4 bits of `c`, so they can be read back without
|
|
978
|
+
* touching the DataTable again.
|
|
979
|
+
* Returns true if the string fit inline; false if it must go to the heap.
|
|
980
|
+
*/
|
|
981
|
+
updateInlineStringSlot(offset: number, value: string): boolean {
|
|
982
|
+
const bytes = this.encoder.encode(value);
|
|
983
|
+
if (bytes.byteLength > 11) return false;
|
|
984
|
+
const buf = this.dataTable;
|
|
985
|
+
buf.setInt32(offset, TYPE_INLINE_STRING);
|
|
986
|
+
// Pack length into the top 4 bits of `c`; bytes 0-3 in `a`, 4-7 in `b`,
|
|
987
|
+
// 8-10 in the low 28 bits of `c`.
|
|
988
|
+
let a = 0;
|
|
989
|
+
let b = 0;
|
|
990
|
+
let c = bytes.byteLength << 28;
|
|
991
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
992
|
+
const byte = bytes[i]!;
|
|
993
|
+
if (i < 4) a |= byte << (i * 8);
|
|
994
|
+
else if (i < 8) b |= byte << ((i - 4) * 8);
|
|
995
|
+
else c |= byte << ((i - 8) * 8);
|
|
996
|
+
}
|
|
997
|
+
buf.setInt32(offset + 4, a);
|
|
998
|
+
buf.setInt32(offset + 8, b);
|
|
999
|
+
buf.setInt32(offset + 12, c);
|
|
1000
|
+
return true;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
727
1003
|
/**
|
|
728
1004
|
* Create an object in the DataTable.
|
|
729
1005
|
*
|
|
@@ -733,8 +1009,7 @@ export class SharedMemory {
|
|
|
733
1009
|
createObject(properties: Array<[string, ValueSlot]>): number {
|
|
734
1010
|
const blockSize =
|
|
735
1011
|
OBJECT_HEADER + properties.length * (NAME_REF_SIZE + SLOT_SIZE);
|
|
736
|
-
const offset = this.dataTable.
|
|
737
|
-
this.dataTable.ensureCapacity(offset + blockSize);
|
|
1012
|
+
const offset = this.dataTable.reserve(blockSize);
|
|
738
1013
|
|
|
739
1014
|
const objectId = this.nextObjectId++;
|
|
740
1015
|
this.dataTable.setInt32(offset, TYPE_OBJECT);
|
|
@@ -750,7 +1025,6 @@ export class SharedMemory {
|
|
|
750
1025
|
writeSlot(this.dataTable, cursor, slot);
|
|
751
1026
|
cursor += SLOT_SIZE;
|
|
752
1027
|
}
|
|
753
|
-
this.dataTable.advance(blockSize);
|
|
754
1028
|
return offset;
|
|
755
1029
|
}
|
|
756
1030
|
|
|
@@ -776,6 +1050,32 @@ export class SharedMemory {
|
|
|
776
1050
|
return this.dataTable.getInt32(offset + 8);
|
|
777
1051
|
}
|
|
778
1052
|
|
|
1053
|
+
/**
|
|
1054
|
+
* Free a single value slot in the DataTable, pushing it onto the free list
|
|
1055
|
+
* so it can be reused. Detects double frees.
|
|
1056
|
+
*/
|
|
1057
|
+
freeValueSlot(_offset: number): void {
|
|
1058
|
+
// No-op: value slots are reclaimed by defrag, not the free list.
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Free an object block in the DataTable, pushing it onto the free list so
|
|
1063
|
+
* it can be reused. Detects double frees. The block's data is overwritten
|
|
1064
|
+
* with free-list metadata, so it must not be read after freeing.
|
|
1065
|
+
*/
|
|
1066
|
+
freeObject(_offset: number): void {
|
|
1067
|
+
// No-op: object blocks are reclaimed by defrag, not the free list.
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* Free a string in the StringTable, pushing it onto the free list so it can
|
|
1072
|
+
* be reused. Detects double frees. The string's bytes are overwritten with
|
|
1073
|
+
* free-list metadata, so it must not be read after freeing.
|
|
1074
|
+
*/
|
|
1075
|
+
freeString(_ref: StringRef): void {
|
|
1076
|
+
// No-op: string blocks are reclaimed by defrag, not the free list.
|
|
1077
|
+
}
|
|
1078
|
+
|
|
779
1079
|
/**
|
|
780
1080
|
* Get the stable layout of an object value: its total DataTable offset and
|
|
781
1081
|
* the slot offset of each property.
|
|
@@ -862,6 +1162,15 @@ export class SharedMemory {
|
|
|
862
1162
|
return null;
|
|
863
1163
|
}
|
|
864
1164
|
|
|
1165
|
+
/**
|
|
1166
|
+
* Int32Array index of a key's version cell within the Index Table buffer.
|
|
1167
|
+
* Stable for the lifetime of the key (the Index Table is never reordered or
|
|
1168
|
+
* replaced), so callers may cache it and load the version directly.
|
|
1169
|
+
*/
|
|
1170
|
+
getVersionCellIndex(index: number): number {
|
|
1171
|
+
return versionCellIndex(index);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
865
1174
|
/** Read the current version for a key index (lock-free). */
|
|
866
1175
|
getVersion(index: number): number {
|
|
867
1176
|
return Atomics.load(this.versionView, versionCellIndex(index));
|
|
@@ -875,7 +1184,13 @@ export class SharedMemory {
|
|
|
875
1184
|
/** Insert a key → dataOffset mapping into the Index Table. */
|
|
876
1185
|
insert(key: string, dataOffset: number): void {
|
|
877
1186
|
const keyRef = this.storeString(key);
|
|
878
|
-
|
|
1187
|
+
// Atomically reserve the next entry index. The count cell (index 0) is
|
|
1188
|
+
// incremented with a single Atomics.add, so concurrent workers each get a
|
|
1189
|
+
// distinct entry slot without a mutex. The Index Table is a single
|
|
1190
|
+
// growable SAB, so a fresh Int32Array view over its current buffer is
|
|
1191
|
+
// correct (insert is rare — only on first write of a key).
|
|
1192
|
+
const view = new Int32Array(this.indexTable.buffers[0]!);
|
|
1193
|
+
const count = Atomics.add(view, 0, 1);
|
|
879
1194
|
const entryOffset = INDEX_HEADER + count * INDEX_ENTRY_SIZE;
|
|
880
1195
|
this.indexTable.ensureCapacity(entryOffset + INDEX_ENTRY_SIZE);
|
|
881
1196
|
this.indexTable.setInt32(entryOffset, keyRef.offset);
|
|
@@ -883,8 +1198,6 @@ export class SharedMemory {
|
|
|
883
1198
|
this.indexTable.setInt32(entryOffset + 8, dataOffset);
|
|
884
1199
|
// The 4th field is the key's version cell, initialized to 0.
|
|
885
1200
|
this.indexTable.setInt32(entryOffset + INDEX_VERSION_OFFSET, 0);
|
|
886
|
-
this.indexTable.setInt32(0, count + 1);
|
|
887
|
-
this.indexTable.advance(INDEX_ENTRY_SIZE);
|
|
888
1201
|
|
|
889
1202
|
// Rebuild the version view over the first chunk (the Index Table is small
|
|
890
1203
|
// and stays within the first chunk for typical workloads; version cells
|
|
@@ -906,6 +1219,49 @@ export class SharedMemory {
|
|
|
906
1219
|
Atomics.add(this.layoutVersion, 0, 1);
|
|
907
1220
|
}
|
|
908
1221
|
|
|
1222
|
+
// ---------------------------------------------------------------------------
|
|
1223
|
+
// Lock-free write coordination (write counter + defrag flag)
|
|
1224
|
+
// ---------------------------------------------------------------------------
|
|
1225
|
+
// Cell 0 of `layoutVersion` is the global layout version. Cells 1 and 2 are
|
|
1226
|
+
// used to coordinate lock-free writes against the (rare) defragmentation
|
|
1227
|
+
// pass: cell 1 is a count of in-flight lock-free writes, cell 2 is a flag
|
|
1228
|
+
// set while a defragmentation pass is running. A write increments the
|
|
1229
|
+
// counter, checks the flag, and if the flag is set it falls back to the
|
|
1230
|
+
// mutex. The defrag sets the flag, waits for the counter to reach zero, then
|
|
1231
|
+
// runs under the mutex.
|
|
1232
|
+
|
|
1233
|
+
/** Enter a lock-free write: increment the in-flight write counter. */
|
|
1234
|
+
beginWrite(): void {
|
|
1235
|
+
Atomics.add(this.layoutVersion, 1, 1);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
/** Leave a lock-free write: decrement the in-flight write counter. */
|
|
1239
|
+
endWrite(): void {
|
|
1240
|
+
Atomics.sub(this.layoutVersion, 1, 1);
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
/** Whether a defragmentation pass is currently running. */
|
|
1244
|
+
isDefragmenting(): boolean {
|
|
1245
|
+
return Atomics.load(this.layoutVersion, 2) !== 0;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
/** Set the defragmentation-in-progress flag. */
|
|
1249
|
+
setDefragmenting(): void {
|
|
1250
|
+
Atomics.store(this.layoutVersion, 2, 1);
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
/** Clear the defragmentation-in-progress flag. */
|
|
1254
|
+
clearDefragmenting(): void {
|
|
1255
|
+
Atomics.store(this.layoutVersion, 2, 0);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/** Wait until no lock-free writes are in flight. */
|
|
1259
|
+
waitForWritesToFinish(): void {
|
|
1260
|
+
while (Atomics.load(this.layoutVersion, 1) !== 0) {
|
|
1261
|
+
Atomics.wait(this.layoutVersion, 1, Atomics.load(this.layoutVersion, 1));
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
909
1265
|
// ---------------------------------------------------------------------------
|
|
910
1266
|
// Defragmentation
|
|
911
1267
|
// ---------------------------------------------------------------------------
|
|
@@ -938,6 +1294,10 @@ export class SharedMemory {
|
|
|
938
1294
|
const liveArrays = new Set<number>();
|
|
939
1295
|
const liveObjects = new Set<number>();
|
|
940
1296
|
const liveDataOffsets = new Set<number>();
|
|
1297
|
+
/** Byte size of each live ArrayTable block (array or string value). */
|
|
1298
|
+
const arrayBlockSizes = new Map<number, number>();
|
|
1299
|
+
/** Offsets of live string-value blocks (no slots to remap). */
|
|
1300
|
+
const stringValueBlocks = new Set<number>();
|
|
941
1301
|
|
|
942
1302
|
const markString = (offset: number) => liveStrings.add(offset);
|
|
943
1303
|
|
|
@@ -947,15 +1307,27 @@ export class SharedMemory {
|
|
|
947
1307
|
case TYPE_BIGINT_STR:
|
|
948
1308
|
liveStrings.add(slot.a);
|
|
949
1309
|
break;
|
|
1310
|
+
case TYPE_STRING_VALUE_REF:
|
|
1311
|
+
liveArrays.add(slot.a);
|
|
1312
|
+
stringValueBlocks.add(slot.a);
|
|
1313
|
+
arrayBlockSizes.set(
|
|
1314
|
+
slot.a,
|
|
1315
|
+
Math.max(ARRAY_HEADER + slot.b, MIN_BLOCK_SIZE),
|
|
1316
|
+
);
|
|
1317
|
+
break;
|
|
950
1318
|
case TYPE_ARRAY_REF:
|
|
951
|
-
markArray(slot.a, slot.b);
|
|
1319
|
+
markArray(slot.a, slot.b, slot.c);
|
|
952
1320
|
break;
|
|
953
1321
|
}
|
|
954
1322
|
};
|
|
955
1323
|
|
|
956
|
-
const markArray = (offset: number, count: number) => {
|
|
1324
|
+
const markArray = (offset: number, count: number, capacity: number) => {
|
|
957
1325
|
if (liveArrays.has(offset)) return;
|
|
958
1326
|
liveArrays.add(offset);
|
|
1327
|
+
arrayBlockSizes.set(
|
|
1328
|
+
offset,
|
|
1329
|
+
Math.max(ARRAY_HEADER + capacity * SLOT_SIZE, MIN_BLOCK_SIZE),
|
|
1330
|
+
);
|
|
959
1331
|
for (let i = 0; i < count; i++) {
|
|
960
1332
|
markValueSlot(
|
|
961
1333
|
readSlot(this.arrayTable, offset + ARRAY_HEADER + i * SLOT_SIZE),
|
|
@@ -1022,19 +1394,10 @@ export class SharedMemory {
|
|
|
1022
1394
|
const sortedArrays = [...liveArrays].sort((a, b) => a - b);
|
|
1023
1395
|
let newArrayOffset = 0;
|
|
1024
1396
|
for (const oldOffset of sortedArrays) {
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
const next = sortedArrays[sortedArrays.indexOf(oldOffset) + 1];
|
|
1028
|
-
const end = next !== undefined ? next : this.arrayTable.byteLength;
|
|
1029
|
-
const capacity = (end - oldOffset - ARRAY_HEADER) / SLOT_SIZE;
|
|
1030
|
-
copyBytes(
|
|
1031
|
-
this.arrayTable,
|
|
1032
|
-
newArrayOffset,
|
|
1033
|
-
oldOffset,
|
|
1034
|
-
ARRAY_HEADER + capacity * SLOT_SIZE,
|
|
1035
|
-
);
|
|
1397
|
+
const size = arrayBlockSizes.get(oldOffset)!;
|
|
1398
|
+
copyBytes(this.arrayTable, newArrayOffset, oldOffset, size);
|
|
1036
1399
|
arrayRemap.set(oldOffset, newArrayOffset);
|
|
1037
|
-
newArrayOffset +=
|
|
1400
|
+
newArrayOffset += size;
|
|
1038
1401
|
}
|
|
1039
1402
|
this.arrayTable.reset(newArrayOffset);
|
|
1040
1403
|
|
|
@@ -1080,10 +1443,10 @@ export class SharedMemory {
|
|
|
1080
1443
|
remapSlot(this.dataTable, newOff, stringRemap, arrayRemap);
|
|
1081
1444
|
}
|
|
1082
1445
|
for (const oldOffset of sortedArrays) {
|
|
1446
|
+
if (stringValueBlocks.has(oldOffset)) continue;
|
|
1083
1447
|
const newOff = arrayRemap.get(oldOffset)!;
|
|
1084
|
-
const
|
|
1085
|
-
|
|
1086
|
-
const capacity = (end - oldOffset - ARRAY_HEADER) / SLOT_SIZE;
|
|
1448
|
+
const capacity =
|
|
1449
|
+
(arrayBlockSizes.get(oldOffset)! - ARRAY_HEADER) / SLOT_SIZE;
|
|
1087
1450
|
for (let i = 0; i < capacity; i++) {
|
|
1088
1451
|
remapSlot(
|
|
1089
1452
|
this.arrayTable,
|
|
@@ -1139,7 +1502,7 @@ function copyBytes(
|
|
|
1139
1502
|
/**
|
|
1140
1503
|
* Rewrite a value slot's string/array references after compaction, in place.
|
|
1141
1504
|
* String refs and bigint-str refs are remapped via `stringRemap`; array refs
|
|
1142
|
-
* via `arrayRemap`.
|
|
1505
|
+
* and string-value refs (which live in the ArrayTable) via `arrayRemap`.
|
|
1143
1506
|
*/
|
|
1144
1507
|
function remapSlot(
|
|
1145
1508
|
buf: GrowableBuffer,
|
|
@@ -1150,7 +1513,7 @@ function remapSlot(
|
|
|
1150
1513
|
const type = buf.getInt32(offset);
|
|
1151
1514
|
if (type === TYPE_STRING_REF || type === TYPE_BIGINT_STR) {
|
|
1152
1515
|
buf.setInt32(offset + 4, stringRemap.get(buf.getInt32(offset + 4))!);
|
|
1153
|
-
} else if (type === TYPE_ARRAY_REF) {
|
|
1516
|
+
} else if (type === TYPE_ARRAY_REF || type === TYPE_STRING_VALUE_REF) {
|
|
1154
1517
|
buf.setInt32(offset + 4, arrayRemap.get(buf.getInt32(offset + 4))!);
|
|
1155
1518
|
}
|
|
1156
1519
|
}
|