bun-shared-memory 0.1.8 → 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 +179 -0
- package/src/memory.ts +371 -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:
|
|
@@ -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,63 @@ 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 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
|
+
|
|
669
845
|
/** Read a single value slot of an array from the ArrayTable. */
|
|
670
846
|
readArraySlot(ref: ArrayRef, index: number): ValueSlot {
|
|
671
847
|
return readSlot(
|
|
@@ -685,10 +861,8 @@ export class SharedMemory {
|
|
|
685
861
|
* the DataTable to point at the new location.
|
|
686
862
|
*/
|
|
687
863
|
moveArray(ref: ArrayRef, newCapacity: number): ArrayRef {
|
|
688
|
-
const
|
|
689
|
-
this.arrayTable.
|
|
690
|
-
newOffset + ARRAY_HEADER + newCapacity * SLOT_SIZE,
|
|
691
|
-
);
|
|
864
|
+
const size = ARRAY_HEADER + newCapacity * SLOT_SIZE;
|
|
865
|
+
const newOffset = this.arrayTable.reserve(size);
|
|
692
866
|
const objectId = this.getArrayObjectId(ref.offset);
|
|
693
867
|
this.arrayTable.setInt32(newOffset, objectId);
|
|
694
868
|
for (let i = 0; i < ref.count; i++) {
|
|
@@ -698,7 +872,13 @@ export class SharedMemory {
|
|
|
698
872
|
readSlot(this.arrayTable, ref.offset + ARRAY_HEADER + i * SLOT_SIZE),
|
|
699
873
|
);
|
|
700
874
|
}
|
|
701
|
-
|
|
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();
|
|
702
882
|
return { offset: newOffset, count: ref.count, capacity: newCapacity };
|
|
703
883
|
}
|
|
704
884
|
|
|
@@ -734,16 +914,23 @@ export class SharedMemory {
|
|
|
734
914
|
};
|
|
735
915
|
}
|
|
736
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
|
+
|
|
737
926
|
// ---------------------------------------------------------------------------
|
|
738
927
|
// DataTable
|
|
739
928
|
// ---------------------------------------------------------------------------
|
|
740
929
|
|
|
741
930
|
/** Store a single value slot in the DataTable and return its offset. */
|
|
742
931
|
storeValueSlot(slot: ValueSlot): number {
|
|
743
|
-
const offset = this.dataTable.
|
|
744
|
-
this.dataTable.ensureCapacity(offset + SLOT_SIZE);
|
|
932
|
+
const offset = this.dataTable.reserve(SLOT_SIZE);
|
|
745
933
|
writeSlot(this.dataTable, offset, slot);
|
|
746
|
-
this.dataTable.advance(SLOT_SIZE);
|
|
747
934
|
return offset;
|
|
748
935
|
}
|
|
749
936
|
|
|
@@ -757,6 +944,62 @@ export class SharedMemory {
|
|
|
757
944
|
writeSlot(this.dataTable, offset, slot);
|
|
758
945
|
}
|
|
759
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
|
+
|
|
760
1003
|
/**
|
|
761
1004
|
* Create an object in the DataTable.
|
|
762
1005
|
*
|
|
@@ -766,8 +1009,7 @@ export class SharedMemory {
|
|
|
766
1009
|
createObject(properties: Array<[string, ValueSlot]>): number {
|
|
767
1010
|
const blockSize =
|
|
768
1011
|
OBJECT_HEADER + properties.length * (NAME_REF_SIZE + SLOT_SIZE);
|
|
769
|
-
const offset = this.dataTable.
|
|
770
|
-
this.dataTable.ensureCapacity(offset + blockSize);
|
|
1012
|
+
const offset = this.dataTable.reserve(blockSize);
|
|
771
1013
|
|
|
772
1014
|
const objectId = this.nextObjectId++;
|
|
773
1015
|
this.dataTable.setInt32(offset, TYPE_OBJECT);
|
|
@@ -783,7 +1025,6 @@ export class SharedMemory {
|
|
|
783
1025
|
writeSlot(this.dataTable, cursor, slot);
|
|
784
1026
|
cursor += SLOT_SIZE;
|
|
785
1027
|
}
|
|
786
|
-
this.dataTable.advance(blockSize);
|
|
787
1028
|
return offset;
|
|
788
1029
|
}
|
|
789
1030
|
|
|
@@ -809,6 +1050,32 @@ export class SharedMemory {
|
|
|
809
1050
|
return this.dataTable.getInt32(offset + 8);
|
|
810
1051
|
}
|
|
811
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
|
+
|
|
812
1079
|
/**
|
|
813
1080
|
* Get the stable layout of an object value: its total DataTable offset and
|
|
814
1081
|
* the slot offset of each property.
|
|
@@ -895,6 +1162,15 @@ export class SharedMemory {
|
|
|
895
1162
|
return null;
|
|
896
1163
|
}
|
|
897
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
|
+
|
|
898
1174
|
/** Read the current version for a key index (lock-free). */
|
|
899
1175
|
getVersion(index: number): number {
|
|
900
1176
|
return Atomics.load(this.versionView, versionCellIndex(index));
|
|
@@ -908,7 +1184,13 @@ export class SharedMemory {
|
|
|
908
1184
|
/** Insert a key → dataOffset mapping into the Index Table. */
|
|
909
1185
|
insert(key: string, dataOffset: number): void {
|
|
910
1186
|
const keyRef = this.storeString(key);
|
|
911
|
-
|
|
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);
|
|
912
1194
|
const entryOffset = INDEX_HEADER + count * INDEX_ENTRY_SIZE;
|
|
913
1195
|
this.indexTable.ensureCapacity(entryOffset + INDEX_ENTRY_SIZE);
|
|
914
1196
|
this.indexTable.setInt32(entryOffset, keyRef.offset);
|
|
@@ -916,8 +1198,6 @@ export class SharedMemory {
|
|
|
916
1198
|
this.indexTable.setInt32(entryOffset + 8, dataOffset);
|
|
917
1199
|
// The 4th field is the key's version cell, initialized to 0.
|
|
918
1200
|
this.indexTable.setInt32(entryOffset + INDEX_VERSION_OFFSET, 0);
|
|
919
|
-
this.indexTable.setInt32(0, count + 1);
|
|
920
|
-
this.indexTable.advance(INDEX_ENTRY_SIZE);
|
|
921
1201
|
|
|
922
1202
|
// Rebuild the version view over the first chunk (the Index Table is small
|
|
923
1203
|
// and stays within the first chunk for typical workloads; version cells
|
|
@@ -939,6 +1219,49 @@ export class SharedMemory {
|
|
|
939
1219
|
Atomics.add(this.layoutVersion, 0, 1);
|
|
940
1220
|
}
|
|
941
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
|
+
|
|
942
1265
|
// ---------------------------------------------------------------------------
|
|
943
1266
|
// Defragmentation
|
|
944
1267
|
// ---------------------------------------------------------------------------
|
|
@@ -971,6 +1294,10 @@ export class SharedMemory {
|
|
|
971
1294
|
const liveArrays = new Set<number>();
|
|
972
1295
|
const liveObjects = new Set<number>();
|
|
973
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>();
|
|
974
1301
|
|
|
975
1302
|
const markString = (offset: number) => liveStrings.add(offset);
|
|
976
1303
|
|
|
@@ -980,15 +1307,27 @@ export class SharedMemory {
|
|
|
980
1307
|
case TYPE_BIGINT_STR:
|
|
981
1308
|
liveStrings.add(slot.a);
|
|
982
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;
|
|
983
1318
|
case TYPE_ARRAY_REF:
|
|
984
|
-
markArray(slot.a, slot.b);
|
|
1319
|
+
markArray(slot.a, slot.b, slot.c);
|
|
985
1320
|
break;
|
|
986
1321
|
}
|
|
987
1322
|
};
|
|
988
1323
|
|
|
989
|
-
const markArray = (offset: number, count: number) => {
|
|
1324
|
+
const markArray = (offset: number, count: number, capacity: number) => {
|
|
990
1325
|
if (liveArrays.has(offset)) return;
|
|
991
1326
|
liveArrays.add(offset);
|
|
1327
|
+
arrayBlockSizes.set(
|
|
1328
|
+
offset,
|
|
1329
|
+
Math.max(ARRAY_HEADER + capacity * SLOT_SIZE, MIN_BLOCK_SIZE),
|
|
1330
|
+
);
|
|
992
1331
|
for (let i = 0; i < count; i++) {
|
|
993
1332
|
markValueSlot(
|
|
994
1333
|
readSlot(this.arrayTable, offset + ARRAY_HEADER + i * SLOT_SIZE),
|
|
@@ -1055,19 +1394,10 @@ export class SharedMemory {
|
|
|
1055
1394
|
const sortedArrays = [...liveArrays].sort((a, b) => a - b);
|
|
1056
1395
|
let newArrayOffset = 0;
|
|
1057
1396
|
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
|
-
);
|
|
1397
|
+
const size = arrayBlockSizes.get(oldOffset)!;
|
|
1398
|
+
copyBytes(this.arrayTable, newArrayOffset, oldOffset, size);
|
|
1069
1399
|
arrayRemap.set(oldOffset, newArrayOffset);
|
|
1070
|
-
newArrayOffset +=
|
|
1400
|
+
newArrayOffset += size;
|
|
1071
1401
|
}
|
|
1072
1402
|
this.arrayTable.reset(newArrayOffset);
|
|
1073
1403
|
|
|
@@ -1113,10 +1443,10 @@ export class SharedMemory {
|
|
|
1113
1443
|
remapSlot(this.dataTable, newOff, stringRemap, arrayRemap);
|
|
1114
1444
|
}
|
|
1115
1445
|
for (const oldOffset of sortedArrays) {
|
|
1446
|
+
if (stringValueBlocks.has(oldOffset)) continue;
|
|
1116
1447
|
const newOff = arrayRemap.get(oldOffset)!;
|
|
1117
|
-
const
|
|
1118
|
-
|
|
1119
|
-
const capacity = (end - oldOffset - ARRAY_HEADER) / SLOT_SIZE;
|
|
1448
|
+
const capacity =
|
|
1449
|
+
(arrayBlockSizes.get(oldOffset)! - ARRAY_HEADER) / SLOT_SIZE;
|
|
1120
1450
|
for (let i = 0; i < capacity; i++) {
|
|
1121
1451
|
remapSlot(
|
|
1122
1452
|
this.arrayTable,
|
|
@@ -1172,7 +1502,7 @@ function copyBytes(
|
|
|
1172
1502
|
/**
|
|
1173
1503
|
* Rewrite a value slot's string/array references after compaction, in place.
|
|
1174
1504
|
* String refs and bigint-str refs are remapped via `stringRemap`; array refs
|
|
1175
|
-
* via `arrayRemap`.
|
|
1505
|
+
* and string-value refs (which live in the ArrayTable) via `arrayRemap`.
|
|
1176
1506
|
*/
|
|
1177
1507
|
function remapSlot(
|
|
1178
1508
|
buf: GrowableBuffer,
|
|
@@ -1183,7 +1513,7 @@ function remapSlot(
|
|
|
1183
1513
|
const type = buf.getInt32(offset);
|
|
1184
1514
|
if (type === TYPE_STRING_REF || type === TYPE_BIGINT_STR) {
|
|
1185
1515
|
buf.setInt32(offset + 4, stringRemap.get(buf.getInt32(offset + 4))!);
|
|
1186
|
-
} else if (type === TYPE_ARRAY_REF) {
|
|
1516
|
+
} else if (type === TYPE_ARRAY_REF || type === TYPE_STRING_VALUE_REF) {
|
|
1187
1517
|
buf.setInt32(offset + 4, arrayRemap.get(buf.getInt32(offset + 4))!);
|
|
1188
1518
|
}
|
|
1189
1519
|
}
|