bun-shared-memory 0.1.8 → 0.2.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/CHANGELOG.md +100 -0
- package/README.md +18 -18
- package/package.json +2 -1
- package/src/growable-buffer.ts +179 -0
- package/src/memory.ts +398 -41
- package/src/refcount.ts +31 -8
- package/src/shared.ts +793 -207
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:
|
|
@@ -217,6 +232,11 @@ export interface SharedMemoryOptions {
|
|
|
217
232
|
stringTable?: SharedArrayBuffer;
|
|
218
233
|
arrayTable?: SharedArrayBuffer;
|
|
219
234
|
};
|
|
235
|
+
freeLists?: {
|
|
236
|
+
dataTable?: SharedArrayBuffer;
|
|
237
|
+
stringTable?: SharedArrayBuffer;
|
|
238
|
+
arrayTable?: SharedArrayBuffer;
|
|
239
|
+
};
|
|
220
240
|
};
|
|
221
241
|
}
|
|
222
242
|
|
|
@@ -230,6 +250,7 @@ function makeBuffer(
|
|
|
230
250
|
singleBuffer = false,
|
|
231
251
|
chargeInitial = true,
|
|
232
252
|
cursorBuffer?: SharedArrayBuffer,
|
|
253
|
+
freeListBuffer?: SharedArrayBuffer,
|
|
233
254
|
): GrowableBuffer {
|
|
234
255
|
if (buf === undefined) {
|
|
235
256
|
return new GrowableBuffer(
|
|
@@ -240,6 +261,7 @@ function makeBuffer(
|
|
|
240
261
|
singleBuffer,
|
|
241
262
|
chargeInitial,
|
|
242
263
|
cursorBuffer,
|
|
264
|
+
freeListBuffer,
|
|
243
265
|
);
|
|
244
266
|
}
|
|
245
267
|
return new GrowableBuffer(
|
|
@@ -250,6 +272,7 @@ function makeBuffer(
|
|
|
250
272
|
singleBuffer,
|
|
251
273
|
chargeInitial,
|
|
252
274
|
cursorBuffer,
|
|
275
|
+
freeListBuffer,
|
|
253
276
|
);
|
|
254
277
|
}
|
|
255
278
|
|
|
@@ -274,7 +297,7 @@ export class SharedMemory {
|
|
|
274
297
|
* on growth. The version is incremented on every write so workers can
|
|
275
298
|
* invalidate their read caches cross-worker.
|
|
276
299
|
*/
|
|
277
|
-
|
|
300
|
+
versionView: Int32Array;
|
|
278
301
|
/**
|
|
279
302
|
* Global layout version cell. Bumped by the main thread after a
|
|
280
303
|
* defragmentation pass so workers can invalidate cached offsets (indexCache,
|
|
@@ -380,6 +403,7 @@ export class SharedMemory {
|
|
|
380
403
|
false,
|
|
381
404
|
chargeInitial,
|
|
382
405
|
buffers?.cursors?.dataTable,
|
|
406
|
+
buffers?.freeLists?.dataTable,
|
|
383
407
|
);
|
|
384
408
|
this.stringTable = makeBuffer(
|
|
385
409
|
buffers?.stringTable,
|
|
@@ -390,6 +414,7 @@ export class SharedMemory {
|
|
|
390
414
|
false,
|
|
391
415
|
chargeInitial,
|
|
392
416
|
buffers?.cursors?.stringTable,
|
|
417
|
+
buffers?.freeLists?.stringTable,
|
|
393
418
|
);
|
|
394
419
|
this.arrayTable = makeBuffer(
|
|
395
420
|
buffers?.arrayTable,
|
|
@@ -400,11 +425,12 @@ export class SharedMemory {
|
|
|
400
425
|
false,
|
|
401
426
|
chargeInitial,
|
|
402
427
|
buffers?.cursors?.arrayTable,
|
|
428
|
+
buffers?.freeLists?.arrayTable,
|
|
403
429
|
);
|
|
404
430
|
this.versionView = new Int32Array(this.indexTable.buffers[0]!);
|
|
405
431
|
this.layoutVersion = new Int32Array(
|
|
406
432
|
buffers?.layoutVersion ??
|
|
407
|
-
new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT),
|
|
433
|
+
new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 3),
|
|
408
434
|
);
|
|
409
435
|
this.lock = options?.lock ?? new Mutex();
|
|
410
436
|
this.refCountBuffer = buffers?.refCount ?? null;
|
|
@@ -473,6 +499,11 @@ export class SharedMemory {
|
|
|
473
499
|
stringTable: SharedArrayBuffer;
|
|
474
500
|
arrayTable: SharedArrayBuffer;
|
|
475
501
|
};
|
|
502
|
+
freeLists: {
|
|
503
|
+
dataTable: SharedArrayBuffer;
|
|
504
|
+
stringTable: SharedArrayBuffer;
|
|
505
|
+
arrayTable: SharedArrayBuffer;
|
|
506
|
+
};
|
|
476
507
|
} {
|
|
477
508
|
return {
|
|
478
509
|
indexTable: this.indexTable.buffers,
|
|
@@ -492,6 +523,11 @@ export class SharedMemory {
|
|
|
492
523
|
stringTable: this.stringTable.cursorBuffer,
|
|
493
524
|
arrayTable: this.arrayTable.cursorBuffer,
|
|
494
525
|
},
|
|
526
|
+
freeLists: {
|
|
527
|
+
dataTable: this.dataTable.freeListBuffer,
|
|
528
|
+
stringTable: this.stringTable.freeListBuffer,
|
|
529
|
+
arrayTable: this.arrayTable.freeListBuffer,
|
|
530
|
+
},
|
|
495
531
|
};
|
|
496
532
|
}
|
|
497
533
|
|
|
@@ -517,6 +553,11 @@ export class SharedMemory {
|
|
|
517
553
|
stringTable?: SharedArrayBuffer;
|
|
518
554
|
arrayTable?: SharedArrayBuffer;
|
|
519
555
|
};
|
|
556
|
+
freeLists?: {
|
|
557
|
+
dataTable?: SharedArrayBuffer;
|
|
558
|
+
stringTable?: SharedArrayBuffer;
|
|
559
|
+
arrayTable?: SharedArrayBuffer;
|
|
560
|
+
};
|
|
520
561
|
}): SharedMemory {
|
|
521
562
|
const indexBuffer = Array.isArray(buffers.indexTable)
|
|
522
563
|
? buffers.indexTable[0]
|
|
@@ -538,6 +579,7 @@ export class SharedMemory {
|
|
|
538
579
|
refCount: buffers.refCount ?? undefined,
|
|
539
580
|
budget: buffers.budget ?? undefined,
|
|
540
581
|
cursors: buffers.cursors,
|
|
582
|
+
freeLists: buffers.freeLists,
|
|
541
583
|
},
|
|
542
584
|
lock: new Mutex(buffers.lock),
|
|
543
585
|
});
|
|
@@ -574,10 +616,8 @@ export class SharedMemory {
|
|
|
574
616
|
}
|
|
575
617
|
|
|
576
618
|
const bytes = this.encoder.encode(value);
|
|
577
|
-
const offset = this.stringTable.byteLength;
|
|
578
|
-
this.stringTable.ensureCapacity(offset + bytes.byteLength);
|
|
619
|
+
const offset = this.stringTable.reserve(bytes.byteLength);
|
|
579
620
|
this.stringTable.writeBytes(offset, bytes);
|
|
580
|
-
this.stringTable.advance(bytes.byteLength);
|
|
581
621
|
const ref: StringRef = { offset, length: bytes.byteLength };
|
|
582
622
|
|
|
583
623
|
// Insert into the cache, growing capacity up to `internMax` before evicting.
|
|
@@ -615,6 +655,86 @@ export class SharedMemory {
|
|
|
615
655
|
return this.decoder.decode(bytes);
|
|
616
656
|
}
|
|
617
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
|
+
|
|
618
738
|
// ---------------------------------------------------------------------------
|
|
619
739
|
// ArrayTable
|
|
620
740
|
// ---------------------------------------------------------------------------
|
|
@@ -631,8 +751,8 @@ export class SharedMemory {
|
|
|
631
751
|
storeArray(slots: ValueSlot[], capacity?: number): ArrayRef {
|
|
632
752
|
const count = slots.length;
|
|
633
753
|
const cap = Math.max(capacity ?? 0, count, 1);
|
|
634
|
-
const
|
|
635
|
-
this.arrayTable.
|
|
754
|
+
const size = ARRAY_HEADER + cap * SLOT_SIZE;
|
|
755
|
+
const offset = this.arrayTable.reserve(size);
|
|
636
756
|
const objectId = this.nextObjectId++;
|
|
637
757
|
this.arrayTable.setInt32(offset, objectId);
|
|
638
758
|
for (let i = 0; i < count; i++) {
|
|
@@ -642,7 +762,6 @@ export class SharedMemory {
|
|
|
642
762
|
slots[i]!,
|
|
643
763
|
);
|
|
644
764
|
}
|
|
645
|
-
this.arrayTable.advance(ARRAY_HEADER + cap * SLOT_SIZE);
|
|
646
765
|
return { offset, count, capacity: cap };
|
|
647
766
|
}
|
|
648
767
|
|
|
@@ -666,6 +785,77 @@ export class SharedMemory {
|
|
|
666
785
|
);
|
|
667
786
|
}
|
|
668
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 an int64 bigint into an array slot directly, avoiding
|
|
817
|
+
* the ValueSlot object allocation and the 4 separate Int32 writes.
|
|
818
|
+
*/
|
|
819
|
+
updateArrayBigIntSlot(ref: ArrayRef, index: number, value: bigint): void {
|
|
820
|
+
slotView.setBigInt64(0, value, true);
|
|
821
|
+
const buf = this.arrayTable;
|
|
822
|
+
const off = ref.offset + ARRAY_HEADER + index * SLOT_SIZE;
|
|
823
|
+
buf.setInt32(off, TYPE_BIGINT);
|
|
824
|
+
buf.setInt32(off + 4, slotView.getInt32(0, true));
|
|
825
|
+
buf.setInt32(off + 8, slotView.getInt32(4, true));
|
|
826
|
+
buf.setInt32(off + 12, 0);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Fast-path: write a short string inline into an array slot (up to 11
|
|
831
|
+
* UTF-8 bytes), avoiding a heap allocation and the string-table write.
|
|
832
|
+
* Returns true if the string fit inline; false if it must go to the heap.
|
|
833
|
+
*/
|
|
834
|
+
updateArrayInlineStringSlot(
|
|
835
|
+
ref: ArrayRef,
|
|
836
|
+
index: number,
|
|
837
|
+
value: string,
|
|
838
|
+
): boolean {
|
|
839
|
+
const bytes = this.encoder.encode(value);
|
|
840
|
+
if (bytes.byteLength > 11) return false;
|
|
841
|
+
const buf = this.arrayTable;
|
|
842
|
+
const off = ref.offset + ARRAY_HEADER + index * SLOT_SIZE;
|
|
843
|
+
buf.setInt32(off, TYPE_INLINE_STRING);
|
|
844
|
+
let a = 0;
|
|
845
|
+
let b = 0;
|
|
846
|
+
let c = bytes.byteLength << 28;
|
|
847
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
848
|
+
const byte = bytes[i]!;
|
|
849
|
+
if (i < 4) a |= byte << (i * 8);
|
|
850
|
+
else if (i < 8) b |= byte << ((i - 4) * 8);
|
|
851
|
+
else c |= byte << ((i - 8) * 8);
|
|
852
|
+
}
|
|
853
|
+
buf.setInt32(off + 4, a);
|
|
854
|
+
buf.setInt32(off + 8, b);
|
|
855
|
+
buf.setInt32(off + 12, c);
|
|
856
|
+
return true;
|
|
857
|
+
}
|
|
858
|
+
|
|
669
859
|
/** Read a single value slot of an array from the ArrayTable. */
|
|
670
860
|
readArraySlot(ref: ArrayRef, index: number): ValueSlot {
|
|
671
861
|
return readSlot(
|
|
@@ -685,10 +875,8 @@ export class SharedMemory {
|
|
|
685
875
|
* the DataTable to point at the new location.
|
|
686
876
|
*/
|
|
687
877
|
moveArray(ref: ArrayRef, newCapacity: number): ArrayRef {
|
|
688
|
-
const
|
|
689
|
-
this.arrayTable.
|
|
690
|
-
newOffset + ARRAY_HEADER + newCapacity * SLOT_SIZE,
|
|
691
|
-
);
|
|
878
|
+
const size = ARRAY_HEADER + newCapacity * SLOT_SIZE;
|
|
879
|
+
const newOffset = this.arrayTable.reserve(size);
|
|
692
880
|
const objectId = this.getArrayObjectId(ref.offset);
|
|
693
881
|
this.arrayTable.setInt32(newOffset, objectId);
|
|
694
882
|
for (let i = 0; i < ref.count; i++) {
|
|
@@ -698,7 +886,13 @@ export class SharedMemory {
|
|
|
698
886
|
readSlot(this.arrayTable, ref.offset + ARRAY_HEADER + i * SLOT_SIZE),
|
|
699
887
|
);
|
|
700
888
|
}
|
|
701
|
-
|
|
889
|
+
// The old block is no longer referenced; it is reclaimed by defrag.
|
|
890
|
+
this.freeArray(ref);
|
|
891
|
+
// The array moved to a new offset, so any cached ArrayRef (e.g. the
|
|
892
|
+
// per-target `__arrayRef` on the write hot path) is now stale. Bump the
|
|
893
|
+
// global layout version so those caches are invalidated, exactly as a
|
|
894
|
+
// defragmentation pass does.
|
|
895
|
+
this.bumpLayoutVersion();
|
|
702
896
|
return { offset: newOffset, count: ref.count, capacity: newCapacity };
|
|
703
897
|
}
|
|
704
898
|
|
|
@@ -734,16 +928,23 @@ export class SharedMemory {
|
|
|
734
928
|
};
|
|
735
929
|
}
|
|
736
930
|
|
|
931
|
+
/**
|
|
932
|
+
* Free an array block, pushing it onto the ArrayTable free list so it can
|
|
933
|
+
* be reused. Detects double frees. The block's data is overwritten with
|
|
934
|
+
* free-list metadata, so it must not be read after freeing.
|
|
935
|
+
*/
|
|
936
|
+
freeArray(_ref: ArrayRef): void {
|
|
937
|
+
// No-op: array blocks are reclaimed by defrag, not the free list.
|
|
938
|
+
}
|
|
939
|
+
|
|
737
940
|
// ---------------------------------------------------------------------------
|
|
738
941
|
// DataTable
|
|
739
942
|
// ---------------------------------------------------------------------------
|
|
740
943
|
|
|
741
944
|
/** Store a single value slot in the DataTable and return its offset. */
|
|
742
945
|
storeValueSlot(slot: ValueSlot): number {
|
|
743
|
-
const offset = this.dataTable.
|
|
744
|
-
this.dataTable.ensureCapacity(offset + SLOT_SIZE);
|
|
946
|
+
const offset = this.dataTable.reserve(SLOT_SIZE);
|
|
745
947
|
writeSlot(this.dataTable, offset, slot);
|
|
746
|
-
this.dataTable.advance(SLOT_SIZE);
|
|
747
948
|
return offset;
|
|
748
949
|
}
|
|
749
950
|
|
|
@@ -757,6 +958,75 @@ export class SharedMemory {
|
|
|
757
958
|
writeSlot(this.dataTable, offset, slot);
|
|
758
959
|
}
|
|
759
960
|
|
|
961
|
+
/**
|
|
962
|
+
* Fast-path: write a float64 number slot directly into the DataTable,
|
|
963
|
+
* avoiding the ValueSlot object allocation and the 4 separate Int32
|
|
964
|
+
* writes of the generic path.
|
|
965
|
+
*/
|
|
966
|
+
updateNumberSlot(offset: number, value: number): void {
|
|
967
|
+
slotView.setFloat64(0, value, true);
|
|
968
|
+
const buf = this.dataTable;
|
|
969
|
+
buf.setInt32(offset, TYPE_NUMBER);
|
|
970
|
+
buf.setInt32(offset + 4, slotView.getInt32(0, true));
|
|
971
|
+
buf.setInt32(offset + 8, slotView.getInt32(4, true));
|
|
972
|
+
buf.setInt32(offset + 12, 0);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* Fast-path: write a boolean slot directly into the DataTable, avoiding
|
|
977
|
+
* the ValueSlot object allocation of the generic path.
|
|
978
|
+
*/
|
|
979
|
+
updateBooleanSlot(offset: number, value: boolean): void {
|
|
980
|
+
const buf = this.dataTable;
|
|
981
|
+
buf.setInt32(offset, TYPE_BOOLEAN);
|
|
982
|
+
buf.setInt32(offset + 4, value ? 1 : 0);
|
|
983
|
+
buf.setInt32(offset + 8, 0);
|
|
984
|
+
buf.setInt32(offset + 12, 0);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* Fast-path: write an int64 bigint slot directly into the DataTable,
|
|
989
|
+
* avoiding the ValueSlot object allocation of the generic path.
|
|
990
|
+
*/
|
|
991
|
+
updateBigIntSlot(offset: number, value: bigint): void {
|
|
992
|
+
slotView.setBigInt64(0, value, true);
|
|
993
|
+
const buf = this.dataTable;
|
|
994
|
+
buf.setInt32(offset, TYPE_BIGINT);
|
|
995
|
+
buf.setInt32(offset + 4, slotView.getInt32(0, true));
|
|
996
|
+
buf.setInt32(offset + 8, slotView.getInt32(4, true));
|
|
997
|
+
buf.setInt32(offset + 12, 0);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* Fast-path: write a short string inline into the slot itself (up to 11
|
|
1002
|
+
* UTF-8 bytes), avoiding a heap allocation and the string-table write.
|
|
1003
|
+
* The bytes are packed into the slot's a/b/c fields (11 bytes) with the
|
|
1004
|
+
* length in the top 4 bits of `c`, so they can be read back without
|
|
1005
|
+
* touching the DataTable again.
|
|
1006
|
+
* Returns true if the string fit inline; false if it must go to the heap.
|
|
1007
|
+
*/
|
|
1008
|
+
updateInlineStringSlot(offset: number, value: string): boolean {
|
|
1009
|
+
const bytes = this.encoder.encode(value);
|
|
1010
|
+
if (bytes.byteLength > 11) return false;
|
|
1011
|
+
const buf = this.dataTable;
|
|
1012
|
+
buf.setInt32(offset, TYPE_INLINE_STRING);
|
|
1013
|
+
// Pack length into the top 4 bits of `c`; bytes 0-3 in `a`, 4-7 in `b`,
|
|
1014
|
+
// 8-10 in the low 28 bits of `c`.
|
|
1015
|
+
let a = 0;
|
|
1016
|
+
let b = 0;
|
|
1017
|
+
let c = bytes.byteLength << 28;
|
|
1018
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
1019
|
+
const byte = bytes[i]!;
|
|
1020
|
+
if (i < 4) a |= byte << (i * 8);
|
|
1021
|
+
else if (i < 8) b |= byte << ((i - 4) * 8);
|
|
1022
|
+
else c |= byte << ((i - 8) * 8);
|
|
1023
|
+
}
|
|
1024
|
+
buf.setInt32(offset + 4, a);
|
|
1025
|
+
buf.setInt32(offset + 8, b);
|
|
1026
|
+
buf.setInt32(offset + 12, c);
|
|
1027
|
+
return true;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
760
1030
|
/**
|
|
761
1031
|
* Create an object in the DataTable.
|
|
762
1032
|
*
|
|
@@ -766,8 +1036,7 @@ export class SharedMemory {
|
|
|
766
1036
|
createObject(properties: Array<[string, ValueSlot]>): number {
|
|
767
1037
|
const blockSize =
|
|
768
1038
|
OBJECT_HEADER + properties.length * (NAME_REF_SIZE + SLOT_SIZE);
|
|
769
|
-
const offset = this.dataTable.
|
|
770
|
-
this.dataTable.ensureCapacity(offset + blockSize);
|
|
1039
|
+
const offset = this.dataTable.reserve(blockSize);
|
|
771
1040
|
|
|
772
1041
|
const objectId = this.nextObjectId++;
|
|
773
1042
|
this.dataTable.setInt32(offset, TYPE_OBJECT);
|
|
@@ -783,7 +1052,6 @@ export class SharedMemory {
|
|
|
783
1052
|
writeSlot(this.dataTable, cursor, slot);
|
|
784
1053
|
cursor += SLOT_SIZE;
|
|
785
1054
|
}
|
|
786
|
-
this.dataTable.advance(blockSize);
|
|
787
1055
|
return offset;
|
|
788
1056
|
}
|
|
789
1057
|
|
|
@@ -809,6 +1077,32 @@ export class SharedMemory {
|
|
|
809
1077
|
return this.dataTable.getInt32(offset + 8);
|
|
810
1078
|
}
|
|
811
1079
|
|
|
1080
|
+
/**
|
|
1081
|
+
* Free a single value slot in the DataTable, pushing it onto the free list
|
|
1082
|
+
* so it can be reused. Detects double frees.
|
|
1083
|
+
*/
|
|
1084
|
+
freeValueSlot(_offset: number): void {
|
|
1085
|
+
// No-op: value slots are reclaimed by defrag, not the free list.
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
/**
|
|
1089
|
+
* Free an object block in the DataTable, pushing it onto the free list so
|
|
1090
|
+
* it can be reused. Detects double frees. The block's data is overwritten
|
|
1091
|
+
* with free-list metadata, so it must not be read after freeing.
|
|
1092
|
+
*/
|
|
1093
|
+
freeObject(_offset: number): void {
|
|
1094
|
+
// No-op: object blocks are reclaimed by defrag, not the free list.
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/**
|
|
1098
|
+
* Free a string in the StringTable, pushing it onto the free list so it can
|
|
1099
|
+
* be reused. Detects double frees. The string's bytes are overwritten with
|
|
1100
|
+
* free-list metadata, so it must not be read after freeing.
|
|
1101
|
+
*/
|
|
1102
|
+
freeString(_ref: StringRef): void {
|
|
1103
|
+
// No-op: string blocks are reclaimed by defrag, not the free list.
|
|
1104
|
+
}
|
|
1105
|
+
|
|
812
1106
|
/**
|
|
813
1107
|
* Get the stable layout of an object value: its total DataTable offset and
|
|
814
1108
|
* the slot offset of each property.
|
|
@@ -895,6 +1189,15 @@ export class SharedMemory {
|
|
|
895
1189
|
return null;
|
|
896
1190
|
}
|
|
897
1191
|
|
|
1192
|
+
/**
|
|
1193
|
+
* Int32Array index of a key's version cell within the Index Table buffer.
|
|
1194
|
+
* Stable for the lifetime of the key (the Index Table is never reordered or
|
|
1195
|
+
* replaced), so callers may cache it and load the version directly.
|
|
1196
|
+
*/
|
|
1197
|
+
getVersionCellIndex(index: number): number {
|
|
1198
|
+
return versionCellIndex(index);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
898
1201
|
/** Read the current version for a key index (lock-free). */
|
|
899
1202
|
getVersion(index: number): number {
|
|
900
1203
|
return Atomics.load(this.versionView, versionCellIndex(index));
|
|
@@ -908,7 +1211,13 @@ export class SharedMemory {
|
|
|
908
1211
|
/** Insert a key → dataOffset mapping into the Index Table. */
|
|
909
1212
|
insert(key: string, dataOffset: number): void {
|
|
910
1213
|
const keyRef = this.storeString(key);
|
|
911
|
-
|
|
1214
|
+
// Atomically reserve the next entry index. The count cell (index 0) is
|
|
1215
|
+
// incremented with a single Atomics.add, so concurrent workers each get a
|
|
1216
|
+
// distinct entry slot without a mutex. The Index Table is a single
|
|
1217
|
+
// growable SAB, so a fresh Int32Array view over its current buffer is
|
|
1218
|
+
// correct (insert is rare — only on first write of a key).
|
|
1219
|
+
const view = new Int32Array(this.indexTable.buffers[0]!);
|
|
1220
|
+
const count = Atomics.add(view, 0, 1);
|
|
912
1221
|
const entryOffset = INDEX_HEADER + count * INDEX_ENTRY_SIZE;
|
|
913
1222
|
this.indexTable.ensureCapacity(entryOffset + INDEX_ENTRY_SIZE);
|
|
914
1223
|
this.indexTable.setInt32(entryOffset, keyRef.offset);
|
|
@@ -916,8 +1225,6 @@ export class SharedMemory {
|
|
|
916
1225
|
this.indexTable.setInt32(entryOffset + 8, dataOffset);
|
|
917
1226
|
// The 4th field is the key's version cell, initialized to 0.
|
|
918
1227
|
this.indexTable.setInt32(entryOffset + INDEX_VERSION_OFFSET, 0);
|
|
919
|
-
this.indexTable.setInt32(0, count + 1);
|
|
920
|
-
this.indexTable.advance(INDEX_ENTRY_SIZE);
|
|
921
1228
|
|
|
922
1229
|
// Rebuild the version view over the first chunk (the Index Table is small
|
|
923
1230
|
// and stays within the first chunk for typical workloads; version cells
|
|
@@ -939,6 +1246,49 @@ export class SharedMemory {
|
|
|
939
1246
|
Atomics.add(this.layoutVersion, 0, 1);
|
|
940
1247
|
}
|
|
941
1248
|
|
|
1249
|
+
// ---------------------------------------------------------------------------
|
|
1250
|
+
// Lock-free write coordination (write counter + defrag flag)
|
|
1251
|
+
// ---------------------------------------------------------------------------
|
|
1252
|
+
// Cell 0 of `layoutVersion` is the global layout version. Cells 1 and 2 are
|
|
1253
|
+
// used to coordinate lock-free writes against the (rare) defragmentation
|
|
1254
|
+
// pass: cell 1 is a count of in-flight lock-free writes, cell 2 is a flag
|
|
1255
|
+
// set while a defragmentation pass is running. A write increments the
|
|
1256
|
+
// counter, checks the flag, and if the flag is set it falls back to the
|
|
1257
|
+
// mutex. The defrag sets the flag, waits for the counter to reach zero, then
|
|
1258
|
+
// runs under the mutex.
|
|
1259
|
+
|
|
1260
|
+
/** Enter a lock-free write: increment the in-flight write counter. */
|
|
1261
|
+
beginWrite(): void {
|
|
1262
|
+
Atomics.add(this.layoutVersion, 1, 1);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
/** Leave a lock-free write: decrement the in-flight write counter. */
|
|
1266
|
+
endWrite(): void {
|
|
1267
|
+
Atomics.sub(this.layoutVersion, 1, 1);
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
/** Whether a defragmentation pass is currently running. */
|
|
1271
|
+
isDefragmenting(): boolean {
|
|
1272
|
+
return Atomics.load(this.layoutVersion, 2) !== 0;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
/** Set the defragmentation-in-progress flag. */
|
|
1276
|
+
setDefragmenting(): void {
|
|
1277
|
+
Atomics.store(this.layoutVersion, 2, 1);
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
/** Clear the defragmentation-in-progress flag. */
|
|
1281
|
+
clearDefragmenting(): void {
|
|
1282
|
+
Atomics.store(this.layoutVersion, 2, 0);
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
/** Wait until no lock-free writes are in flight. */
|
|
1286
|
+
waitForWritesToFinish(): void {
|
|
1287
|
+
while (Atomics.load(this.layoutVersion, 1) !== 0) {
|
|
1288
|
+
Atomics.wait(this.layoutVersion, 1, Atomics.load(this.layoutVersion, 1));
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
942
1292
|
// ---------------------------------------------------------------------------
|
|
943
1293
|
// Defragmentation
|
|
944
1294
|
// ---------------------------------------------------------------------------
|
|
@@ -971,6 +1321,10 @@ export class SharedMemory {
|
|
|
971
1321
|
const liveArrays = new Set<number>();
|
|
972
1322
|
const liveObjects = new Set<number>();
|
|
973
1323
|
const liveDataOffsets = new Set<number>();
|
|
1324
|
+
/** Byte size of each live ArrayTable block (array or string value). */
|
|
1325
|
+
const arrayBlockSizes = new Map<number, number>();
|
|
1326
|
+
/** Offsets of live string-value blocks (no slots to remap). */
|
|
1327
|
+
const stringValueBlocks = new Set<number>();
|
|
974
1328
|
|
|
975
1329
|
const markString = (offset: number) => liveStrings.add(offset);
|
|
976
1330
|
|
|
@@ -980,15 +1334,27 @@ export class SharedMemory {
|
|
|
980
1334
|
case TYPE_BIGINT_STR:
|
|
981
1335
|
liveStrings.add(slot.a);
|
|
982
1336
|
break;
|
|
1337
|
+
case TYPE_STRING_VALUE_REF:
|
|
1338
|
+
liveArrays.add(slot.a);
|
|
1339
|
+
stringValueBlocks.add(slot.a);
|
|
1340
|
+
arrayBlockSizes.set(
|
|
1341
|
+
slot.a,
|
|
1342
|
+
Math.max(ARRAY_HEADER + slot.b, MIN_BLOCK_SIZE),
|
|
1343
|
+
);
|
|
1344
|
+
break;
|
|
983
1345
|
case TYPE_ARRAY_REF:
|
|
984
|
-
markArray(slot.a, slot.b);
|
|
1346
|
+
markArray(slot.a, slot.b, slot.c);
|
|
985
1347
|
break;
|
|
986
1348
|
}
|
|
987
1349
|
};
|
|
988
1350
|
|
|
989
|
-
const markArray = (offset: number, count: number) => {
|
|
1351
|
+
const markArray = (offset: number, count: number, capacity: number) => {
|
|
990
1352
|
if (liveArrays.has(offset)) return;
|
|
991
1353
|
liveArrays.add(offset);
|
|
1354
|
+
arrayBlockSizes.set(
|
|
1355
|
+
offset,
|
|
1356
|
+
Math.max(ARRAY_HEADER + capacity * SLOT_SIZE, MIN_BLOCK_SIZE),
|
|
1357
|
+
);
|
|
992
1358
|
for (let i = 0; i < count; i++) {
|
|
993
1359
|
markValueSlot(
|
|
994
1360
|
readSlot(this.arrayTable, offset + ARRAY_HEADER + i * SLOT_SIZE),
|
|
@@ -1055,19 +1421,10 @@ export class SharedMemory {
|
|
|
1055
1421
|
const sortedArrays = [...liveArrays].sort((a, b) => a - b);
|
|
1056
1422
|
let newArrayOffset = 0;
|
|
1057
1423
|
for (const oldOffset of sortedArrays) {
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
const next = sortedArrays[sortedArrays.indexOf(oldOffset) + 1];
|
|
1061
|
-
const end = next !== undefined ? next : this.arrayTable.byteLength;
|
|
1062
|
-
const capacity = (end - oldOffset - ARRAY_HEADER) / SLOT_SIZE;
|
|
1063
|
-
copyBytes(
|
|
1064
|
-
this.arrayTable,
|
|
1065
|
-
newArrayOffset,
|
|
1066
|
-
oldOffset,
|
|
1067
|
-
ARRAY_HEADER + capacity * SLOT_SIZE,
|
|
1068
|
-
);
|
|
1424
|
+
const size = arrayBlockSizes.get(oldOffset)!;
|
|
1425
|
+
copyBytes(this.arrayTable, newArrayOffset, oldOffset, size);
|
|
1069
1426
|
arrayRemap.set(oldOffset, newArrayOffset);
|
|
1070
|
-
newArrayOffset +=
|
|
1427
|
+
newArrayOffset += size;
|
|
1071
1428
|
}
|
|
1072
1429
|
this.arrayTable.reset(newArrayOffset);
|
|
1073
1430
|
|
|
@@ -1113,10 +1470,10 @@ export class SharedMemory {
|
|
|
1113
1470
|
remapSlot(this.dataTable, newOff, stringRemap, arrayRemap);
|
|
1114
1471
|
}
|
|
1115
1472
|
for (const oldOffset of sortedArrays) {
|
|
1473
|
+
if (stringValueBlocks.has(oldOffset)) continue;
|
|
1116
1474
|
const newOff = arrayRemap.get(oldOffset)!;
|
|
1117
|
-
const
|
|
1118
|
-
|
|
1119
|
-
const capacity = (end - oldOffset - ARRAY_HEADER) / SLOT_SIZE;
|
|
1475
|
+
const capacity =
|
|
1476
|
+
(arrayBlockSizes.get(oldOffset)! - ARRAY_HEADER) / SLOT_SIZE;
|
|
1120
1477
|
for (let i = 0; i < capacity; i++) {
|
|
1121
1478
|
remapSlot(
|
|
1122
1479
|
this.arrayTable,
|
|
@@ -1172,7 +1529,7 @@ function copyBytes(
|
|
|
1172
1529
|
/**
|
|
1173
1530
|
* Rewrite a value slot's string/array references after compaction, in place.
|
|
1174
1531
|
* String refs and bigint-str refs are remapped via `stringRemap`; array refs
|
|
1175
|
-
* via `arrayRemap`.
|
|
1532
|
+
* and string-value refs (which live in the ArrayTable) via `arrayRemap`.
|
|
1176
1533
|
*/
|
|
1177
1534
|
function remapSlot(
|
|
1178
1535
|
buf: GrowableBuffer,
|
|
@@ -1183,7 +1540,7 @@ function remapSlot(
|
|
|
1183
1540
|
const type = buf.getInt32(offset);
|
|
1184
1541
|
if (type === TYPE_STRING_REF || type === TYPE_BIGINT_STR) {
|
|
1185
1542
|
buf.setInt32(offset + 4, stringRemap.get(buf.getInt32(offset + 4))!);
|
|
1186
|
-
} else if (type === TYPE_ARRAY_REF) {
|
|
1543
|
+
} else if (type === TYPE_ARRAY_REF || type === TYPE_STRING_VALUE_REF) {
|
|
1187
1544
|
buf.setInt32(offset + 4, arrayRemap.get(buf.getInt32(offset + 4))!);
|
|
1188
1545
|
}
|
|
1189
1546
|
}
|