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/shared.ts
CHANGED
|
@@ -2,6 +2,9 @@ import {
|
|
|
2
2
|
SharedMemory,
|
|
3
3
|
TYPE_ARRAY_REF,
|
|
4
4
|
TYPE_BIGINT_STR,
|
|
5
|
+
TYPE_INLINE_STRING,
|
|
6
|
+
TYPE_STRING_REF,
|
|
7
|
+
TYPE_STRING_VALUE_REF,
|
|
5
8
|
arrayRefSlot,
|
|
6
9
|
bigintSlot,
|
|
7
10
|
bigintStrSlot,
|
|
@@ -9,6 +12,7 @@ import {
|
|
|
9
12
|
decodeSlot,
|
|
10
13
|
numberSlot,
|
|
11
14
|
stringRefSlot,
|
|
15
|
+
stringValueRefSlot,
|
|
12
16
|
type ArrayRef,
|
|
13
17
|
type ObjectLayout,
|
|
14
18
|
type StringRef,
|
|
@@ -32,6 +36,26 @@ const MUTATING_METHODS = new Set([
|
|
|
32
36
|
|
|
33
37
|
type Primitive = number | string | boolean | bigint;
|
|
34
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Run a write operation. Writes are lock-free by default: they increment the
|
|
41
|
+
* in-flight write counter, and only fall back to the shared mutex if a
|
|
42
|
+
* defragmentation pass is running (rare). This keeps the hot write path free
|
|
43
|
+
* of the mutex's re-entrancy tracking while still preventing a write from
|
|
44
|
+
* racing with a defragmentation pass.
|
|
45
|
+
*/
|
|
46
|
+
function writeLock<T>(mem: SharedMemory, fn: () => T): T {
|
|
47
|
+
mem.beginWrite();
|
|
48
|
+
if (mem.isDefragmenting()) {
|
|
49
|
+
mem.endWrite();
|
|
50
|
+
return mem.withLock(fn);
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
return fn();
|
|
54
|
+
} finally {
|
|
55
|
+
mem.endWrite();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
35
59
|
/** Inclusive bounds of a signed 64-bit integer (stored as a fast int64 slot). */
|
|
36
60
|
const MIN_INT64 = -(2n ** 63n);
|
|
37
61
|
const MAX_INT64 = 2n ** 63n - 1n;
|
|
@@ -41,6 +65,7 @@ function encodeValue(
|
|
|
41
65
|
mem: SharedMemory,
|
|
42
66
|
value: unknown,
|
|
43
67
|
arrayCapacity?: number,
|
|
68
|
+
oldSlot?: ValueSlot,
|
|
44
69
|
): ValueSlot {
|
|
45
70
|
switch (typeof value) {
|
|
46
71
|
case "number":
|
|
@@ -55,7 +80,12 @@ function encodeValue(
|
|
|
55
80
|
}
|
|
56
81
|
return bigintStrSlot(mem.storeString(value.toString()));
|
|
57
82
|
case "string":
|
|
58
|
-
|
|
83
|
+
// Reuse the existing string block in place when the new string fits.
|
|
84
|
+
const oldRef =
|
|
85
|
+
oldSlot?.type === TYPE_STRING_VALUE_REF
|
|
86
|
+
? { offset: oldSlot.a, length: oldSlot.b }
|
|
87
|
+
: undefined;
|
|
88
|
+
return stringValueRefSlot(mem.storeStringValue(value, oldRef));
|
|
59
89
|
default:
|
|
60
90
|
if (Array.isArray(value)) {
|
|
61
91
|
for (const v of value) {
|
|
@@ -77,8 +107,12 @@ function encodeValue(
|
|
|
77
107
|
/** Decode a ValueSlot into a JS value, reading strings/arrays from tables. */
|
|
78
108
|
function decodeValue(mem: SharedMemory, slot: ValueSlot): unknown {
|
|
79
109
|
switch (slot.type) {
|
|
80
|
-
case
|
|
110
|
+
case TYPE_STRING_REF: // property-name/string-table ref
|
|
81
111
|
return mem.readString({ offset: slot.a, length: slot.b });
|
|
112
|
+
case TYPE_STRING_VALUE_REF: // string value in the ArrayTable
|
|
113
|
+
return mem.readStringValue({ offset: slot.a, length: slot.b });
|
|
114
|
+
case TYPE_INLINE_STRING: // short string stored inline in the slot
|
|
115
|
+
return mem.readInlineString(slot.a, slot.b, slot.c);
|
|
82
116
|
case TYPE_BIGINT_STR: // bigint stored as decimal string
|
|
83
117
|
return BigInt(mem.readString({ offset: slot.a, length: slot.b }));
|
|
84
118
|
case 4: // array-ref
|
|
@@ -90,6 +124,18 @@ function decodeValue(mem: SharedMemory, slot: ValueSlot): unknown {
|
|
|
90
124
|
}
|
|
91
125
|
}
|
|
92
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Free the blocks referenced by a value slot (an array block or a string),
|
|
129
|
+
* so they can be reused by the slab allocator. Called before a slot is
|
|
130
|
+
* overwritten with a new value. No-op for primitive slots.
|
|
131
|
+
*
|
|
132
|
+
* No-op: string-value/array/object blocks are no longer freed on the hot
|
|
133
|
+
* path (they are reclaimed by defragmentation instead).
|
|
134
|
+
*/
|
|
135
|
+
function freeSlotRefs(_mem: SharedMemory, _slot: ValueSlot): void {
|
|
136
|
+
// Intentionally a no-op; dead blocks are reclaimed by defrag.
|
|
137
|
+
}
|
|
138
|
+
|
|
93
139
|
/**
|
|
94
140
|
* A reference to a value stored in shared memory.
|
|
95
141
|
*
|
|
@@ -137,6 +183,18 @@ const readCache = new Map<
|
|
|
137
183
|
*/
|
|
138
184
|
const indexCache = new Map<string, number>();
|
|
139
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Per-worker cache of a key's current DataTable offset, valid only for the
|
|
188
|
+
* layout version it was resolved under. The offset changes on
|
|
189
|
+
* GC/defragmentation, so it is re-validated against the global layout version
|
|
190
|
+
* on each access (a single Atomics.load). This avoids the Index-Table read on
|
|
191
|
+
* every write, which is the dominant cost of the write hot path.
|
|
192
|
+
*/
|
|
193
|
+
const offsetCache = new Map<
|
|
194
|
+
string,
|
|
195
|
+
{ offset: number; layoutVersion: number }
|
|
196
|
+
>();
|
|
197
|
+
|
|
140
198
|
/** Resolve a key's stable index, caching it after the first lookup. */
|
|
141
199
|
function getIndex(mem: SharedMemory, key: string): number {
|
|
142
200
|
const cached = indexCache.get(key);
|
|
@@ -166,6 +224,65 @@ function getOffset(mem: SharedMemory, key: string): number | null {
|
|
|
166
224
|
return mem.lookupByIndex(index);
|
|
167
225
|
}
|
|
168
226
|
|
|
227
|
+
/**
|
|
228
|
+
* The per-key cache fields on a SharedRef proxy target that the write hot
|
|
229
|
+
* paths use to skip Map lookups and DataTable reads. `__offset`/`__type` are
|
|
230
|
+
* valid only while `__layoutVersion` matches the global layout version;
|
|
231
|
+
* `__arrayRef` additionally requires `__arrayRefLayoutVersion` to match (it is
|
|
232
|
+
* bumped on defragmentation and on array growth via `moveArray`).
|
|
233
|
+
*/
|
|
234
|
+
interface WriteTarget {
|
|
235
|
+
__offset?: number;
|
|
236
|
+
__layoutVersion: number;
|
|
237
|
+
__type?: number;
|
|
238
|
+
__arrayRef?: { offset: number; count: number; capacity: number } | undefined;
|
|
239
|
+
__arrayRefLayoutVersion: number;
|
|
240
|
+
__index?: number | undefined;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Resolve a key's current DataTable offset, caching it across writes. The
|
|
245
|
+
* cached offset is only reused when the global layout version is unchanged
|
|
246
|
+
* (i.e. no defragmentation has moved the data since it was cached); otherwise
|
|
247
|
+
* it is re-resolved from the Index Table.
|
|
248
|
+
*/
|
|
249
|
+
function getCachedOffset(
|
|
250
|
+
mem: SharedMemory,
|
|
251
|
+
key: string,
|
|
252
|
+
target?: WriteTarget,
|
|
253
|
+
): number | null {
|
|
254
|
+
if (target !== undefined && target.__offset !== undefined) {
|
|
255
|
+
if (target.__layoutVersion === mem.getLayoutVersion()) {
|
|
256
|
+
return target.__offset;
|
|
257
|
+
}
|
|
258
|
+
target.__offset = undefined;
|
|
259
|
+
target.__type = undefined;
|
|
260
|
+
target.__arrayRef = undefined;
|
|
261
|
+
}
|
|
262
|
+
const cached = offsetCache.get(key);
|
|
263
|
+
if (cached !== undefined) {
|
|
264
|
+
if (cached.layoutVersion === mem.getLayoutVersion()) {
|
|
265
|
+
if (target !== undefined) {
|
|
266
|
+
target.__offset = cached.offset;
|
|
267
|
+
target.__layoutVersion = cached.layoutVersion;
|
|
268
|
+
}
|
|
269
|
+
return cached.offset;
|
|
270
|
+
}
|
|
271
|
+
offsetCache.delete(key);
|
|
272
|
+
}
|
|
273
|
+
const offset = getOffset(mem, key);
|
|
274
|
+
if (offset !== null) {
|
|
275
|
+
offsetCache.set(key, { offset, layoutVersion: mem.getLayoutVersion() });
|
|
276
|
+
if (target !== undefined) {
|
|
277
|
+
target.__offset = offset;
|
|
278
|
+
target.__layoutVersion = mem.getLayoutVersion();
|
|
279
|
+
target.__type = undefined;
|
|
280
|
+
target.__arrayRef = undefined;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return offset;
|
|
284
|
+
}
|
|
285
|
+
|
|
169
286
|
/** Whether a decoded value is a reference-countable object/array. */
|
|
170
287
|
function isTrackable(value: unknown): boolean {
|
|
171
288
|
return value !== null && typeof value === "object";
|
|
@@ -326,8 +443,14 @@ function readProp(
|
|
|
326
443
|
prop: string | symbol,
|
|
327
444
|
arrayCapacity?: number,
|
|
328
445
|
): unknown {
|
|
329
|
-
|
|
330
|
-
|
|
446
|
+
// Inline the read-cache fast path (instead of calling readCachedValue, which
|
|
447
|
+
// returns a polymorphic {hit,value} union and allocates an object on every
|
|
448
|
+
// cache hit). Keeping this monomorphic lets the JIT optimize the hot read.
|
|
449
|
+
const cached = readCache.get(key);
|
|
450
|
+
const value =
|
|
451
|
+
cached !== undefined && cached.version === mem.getVersion(cached.index)
|
|
452
|
+
? cached.value
|
|
453
|
+
: readValue(mem, key);
|
|
331
454
|
if (Array.isArray(value)) {
|
|
332
455
|
return getTopLevelArrayMember(mem, key, value, prop, arrayCapacity);
|
|
333
456
|
}
|
|
@@ -338,6 +461,94 @@ function readProp(
|
|
|
338
461
|
return propValue;
|
|
339
462
|
}
|
|
340
463
|
|
|
464
|
+
/**
|
|
465
|
+
* Read a single object property for the per-property accessor cache, decoding
|
|
466
|
+
* only the requested slot (not the whole object) and caching the decoded value
|
|
467
|
+
* in `fieldCache` under the observed version. Array properties are cached as
|
|
468
|
+
* an array proxy so subsequent reads hit the fast path.
|
|
469
|
+
*
|
|
470
|
+
* This is the miss path for the object accessor getters; the hit path returns
|
|
471
|
+
* `fieldCache.value` directly.
|
|
472
|
+
*/
|
|
473
|
+
function readObjectProp(
|
|
474
|
+
mem: SharedMemory,
|
|
475
|
+
key: string,
|
|
476
|
+
prop: string,
|
|
477
|
+
fieldCache: { version: number; value: unknown },
|
|
478
|
+
version: number,
|
|
479
|
+
arrayCapacity?: number,
|
|
480
|
+
): unknown {
|
|
481
|
+
const layout = getObjectLayout(mem, key);
|
|
482
|
+
if (layout === null) {
|
|
483
|
+
// Not an object (shouldn't happen for an object accessor); fall back.
|
|
484
|
+
return readProp(mem, key, prop, arrayCapacity);
|
|
485
|
+
}
|
|
486
|
+
const slotOffset = layout.properties.get(prop);
|
|
487
|
+
if (slotOffset === undefined) {
|
|
488
|
+
throw new TypeError(
|
|
489
|
+
`Value for key "${key}" has a fixed shape: properties may not be added or removed`,
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
const slot = mem.readValueSlot(slotOffset);
|
|
493
|
+
let value = decodeValue(mem, slot);
|
|
494
|
+
if (Array.isArray(value)) {
|
|
495
|
+
value = createArrayProxy(mem, key, prop, arrayCapacity);
|
|
496
|
+
}
|
|
497
|
+
fieldCache.version = version;
|
|
498
|
+
fieldCache.value = value;
|
|
499
|
+
return value;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Write a single object-property slot whose offset is already resolved
|
|
504
|
+
* (cached by the caller, e.g. the accessor setter). Avoids the
|
|
505
|
+
* `getObjectLayout` Map lookup + `properties.get` per write on the hot path.
|
|
506
|
+
*/
|
|
507
|
+
function writeObjectSlot(
|
|
508
|
+
mem: SharedMemory,
|
|
509
|
+
key: string,
|
|
510
|
+
slotOffset: number,
|
|
511
|
+
val: unknown,
|
|
512
|
+
arrayCapacity?: number,
|
|
513
|
+
target?: WriteTarget,
|
|
514
|
+
): void {
|
|
515
|
+
if (typeof val === "number") {
|
|
516
|
+
// Fast path: numbers need no old-slot read (freeSlotRefs is a no-op)
|
|
517
|
+
// and no in-place string reuse; write the float64 directly.
|
|
518
|
+
mem.updateNumberSlot(slotOffset, val);
|
|
519
|
+
bumpTarget(mem, key, target);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
if (typeof val === "boolean") {
|
|
523
|
+
// Fast path: booleans need no old-slot read (freeSlotRefs is a no-op)
|
|
524
|
+
// and no in-place string reuse; write the 4 Int32s directly.
|
|
525
|
+
mem.updateBooleanSlot(slotOffset, val);
|
|
526
|
+
bumpTarget(mem, key, target);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
if (typeof val === "bigint" && val >= MIN_INT64 && val <= MAX_INT64) {
|
|
530
|
+
// Fast path: int64 bigints are written directly, avoiding the ValueSlot
|
|
531
|
+
// object allocation of the generic path.
|
|
532
|
+
mem.updateBigIntSlot(slotOffset, val);
|
|
533
|
+
bumpTarget(mem, key, target);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
if (typeof val === "string") {
|
|
537
|
+
// Fast path: short strings are written inline into the slot itself.
|
|
538
|
+
if (mem.updateInlineStringSlot(slotOffset, val)) {
|
|
539
|
+
bumpTarget(mem, key, target);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
const oldSlot = mem.readValueSlot(slotOffset);
|
|
544
|
+
freeSlotRefs(mem, oldSlot);
|
|
545
|
+
mem.updateValueSlot(
|
|
546
|
+
slotOffset,
|
|
547
|
+
encodeValue(mem, val, arrayCapacity, oldSlot),
|
|
548
|
+
);
|
|
549
|
+
bumpTarget(mem, key, target);
|
|
550
|
+
}
|
|
551
|
+
|
|
341
552
|
/**
|
|
342
553
|
* Write a single property of a shared object value in place.
|
|
343
554
|
*
|
|
@@ -350,8 +561,36 @@ function writeProp(
|
|
|
350
561
|
prop: string | symbol,
|
|
351
562
|
val: unknown,
|
|
352
563
|
arrayCapacity?: number,
|
|
564
|
+
target?: WriteTarget,
|
|
353
565
|
): void {
|
|
354
|
-
|
|
566
|
+
// Inline writeLock to avoid a closure allocation on the hot path.
|
|
567
|
+
mem.beginWrite();
|
|
568
|
+
if (mem.isDefragmenting()) {
|
|
569
|
+
mem.endWrite();
|
|
570
|
+
mem.withLock(() =>
|
|
571
|
+
writePropInner(mem, key, prop, val, arrayCapacity, target),
|
|
572
|
+
);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
try {
|
|
576
|
+
writePropInner(mem, key, prop, val, arrayCapacity, target);
|
|
577
|
+
} finally {
|
|
578
|
+
mem.endWrite();
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function writePropInner(
|
|
583
|
+
mem: SharedMemory,
|
|
584
|
+
key: string,
|
|
585
|
+
prop: string | symbol,
|
|
586
|
+
val: unknown,
|
|
587
|
+
arrayCapacity?: number,
|
|
588
|
+
target?: WriteTarget,
|
|
589
|
+
): void {
|
|
590
|
+
// If the slot type is already cached on the target, we can skip the
|
|
591
|
+
// getObjectLayout Map lookup for top-level arrays (which are never objects).
|
|
592
|
+
const knownType = target?.__type;
|
|
593
|
+
if (knownType !== TYPE_ARRAY_REF) {
|
|
355
594
|
// Object property: use the cached layout to write only that slot.
|
|
356
595
|
const layout = getObjectLayout(mem, key);
|
|
357
596
|
if (layout !== null) {
|
|
@@ -361,70 +600,123 @@ function writeProp(
|
|
|
361
600
|
`Value for key "${key}" has a fixed shape: properties may not be added or removed`,
|
|
362
601
|
);
|
|
363
602
|
}
|
|
364
|
-
mem
|
|
365
|
-
bump(mem, key);
|
|
603
|
+
writeObjectSlot(mem, key, slotOffset, val, arrayCapacity, target);
|
|
366
604
|
return;
|
|
367
605
|
}
|
|
606
|
+
}
|
|
368
607
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
608
|
+
// Top-level array index write: update only that array slot.
|
|
609
|
+
const offset = getCachedOffset(mem, key, target);
|
|
610
|
+
if (offset === null) {
|
|
611
|
+
throw new Error(`No shared value found for key "${key}"`);
|
|
612
|
+
}
|
|
613
|
+
const slotType =
|
|
614
|
+
target !== undefined && target.__type !== undefined
|
|
615
|
+
? target.__type
|
|
616
|
+
: mem.dataTable.getInt32(offset);
|
|
617
|
+
if (target !== undefined && target.__type === undefined) {
|
|
618
|
+
target.__type = slotType;
|
|
619
|
+
}
|
|
620
|
+
if (
|
|
621
|
+
slotType === TYPE_ARRAY_REF &&
|
|
622
|
+
typeof prop === "string" &&
|
|
623
|
+
/^\d+$/.test(prop)
|
|
624
|
+
) {
|
|
625
|
+
const index = Number(prop);
|
|
626
|
+
// Use the cached ArrayRef when it is still valid (same layout version,
|
|
627
|
+
// i.e. no defragmentation or array growth has moved it). Otherwise
|
|
628
|
+
// re-read the 3 header Int32s and refresh the cache.
|
|
629
|
+
let ref = target?.__arrayRef;
|
|
375
630
|
if (
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
/^\d+$/.test(prop)
|
|
631
|
+
ref === undefined ||
|
|
632
|
+
target!.__arrayRefLayoutVersion !== mem.getLayoutVersion()
|
|
379
633
|
) {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
634
|
+
ref = {
|
|
635
|
+
offset: mem.dataTable.getInt32(offset + 4),
|
|
636
|
+
count: mem.dataTable.getInt32(offset + 8),
|
|
637
|
+
capacity: mem.dataTable.getInt32(offset + 12),
|
|
638
|
+
};
|
|
639
|
+
if (target !== undefined) {
|
|
640
|
+
target.__arrayRef = ref;
|
|
641
|
+
target.__arrayRefLayoutVersion = mem.getLayoutVersion();
|
|
386
642
|
}
|
|
387
|
-
|
|
388
|
-
|
|
643
|
+
}
|
|
644
|
+
if (index >= ref.count) {
|
|
645
|
+
throw new RangeError(
|
|
646
|
+
`Index ${index} out of bounds for array of length ${ref.count}`,
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
if (typeof val === "number") {
|
|
650
|
+
// Fast path: numbers need no old-slot read (freeSlotRefs is a no-op).
|
|
651
|
+
mem.updateArrayNumberSlot(ref, index, val);
|
|
652
|
+
bumpTarget(mem, key, target);
|
|
389
653
|
return;
|
|
390
654
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
);
|
|
408
|
-
bump(mem, key);
|
|
655
|
+
if (typeof val === "boolean") {
|
|
656
|
+
// Fast path: booleans need no old-slot read (freeSlotRefs is a no-op).
|
|
657
|
+
mem.updateArrayBooleanSlot(ref, index, val);
|
|
658
|
+
bumpTarget(mem, key, target);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
if (typeof val === "bigint" && val >= MIN_INT64 && val <= MAX_INT64) {
|
|
662
|
+
// Fast path: int64 bigints are written directly, avoiding the
|
|
663
|
+
// ValueSlot object allocation of the generic path.
|
|
664
|
+
mem.updateArrayBigIntSlot(ref, index, val);
|
|
665
|
+
bumpTarget(mem, key, target);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
if (typeof val === "string") {
|
|
669
|
+
// Fast path: short strings are written inline into the array slot.
|
|
670
|
+
if (mem.updateArrayInlineStringSlot(ref, index, val)) {
|
|
671
|
+
bumpTarget(mem, key, target);
|
|
409
672
|
return;
|
|
410
673
|
}
|
|
411
674
|
}
|
|
675
|
+
freeSlotRefs(mem, mem.readArraySlot(ref, index));
|
|
676
|
+
mem.updateArraySlot(ref, index, encodeValue(mem, val));
|
|
677
|
+
bumpTarget(mem, key, target);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
412
680
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
(
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
681
|
+
// Top-level array length write: shrink/grow the array in place.
|
|
682
|
+
if (slotType === TYPE_ARRAY_REF && prop === "length") {
|
|
683
|
+
const newLen = Number(val);
|
|
684
|
+
if (!Number.isInteger(newLen) || newLen < 0) {
|
|
685
|
+
throw new RangeError("Invalid array length");
|
|
686
|
+
}
|
|
687
|
+
const ref = {
|
|
688
|
+
offset: mem.dataTable.getInt32(offset + 4),
|
|
689
|
+
count: mem.dataTable.getInt32(offset + 8),
|
|
690
|
+
capacity: mem.dataTable.getInt32(offset + 12),
|
|
691
|
+
};
|
|
692
|
+
if (newLen <= ref.capacity) {
|
|
693
|
+
mem.updateValueSlot(
|
|
694
|
+
offset,
|
|
695
|
+
arrayRefSlot({
|
|
696
|
+
offset: ref.offset,
|
|
697
|
+
count: newLen,
|
|
698
|
+
capacity: ref.capacity,
|
|
699
|
+
}),
|
|
700
|
+
);
|
|
701
|
+
bumpTarget(mem, key, target);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Fallback: full read-modify-write.
|
|
707
|
+
mutateValue<unknown, boolean>(
|
|
708
|
+
mem,
|
|
709
|
+
key,
|
|
710
|
+
(value) => {
|
|
711
|
+
if (Array.isArray(value)) {
|
|
712
|
+
(value as unknown as Record<string | symbol, unknown>)[prop] = val;
|
|
713
|
+
} else {
|
|
714
|
+
(value as Record<string | symbol, unknown>)[prop] = val;
|
|
715
|
+
}
|
|
716
|
+
return true;
|
|
717
|
+
},
|
|
718
|
+
arrayCapacity,
|
|
719
|
+
);
|
|
428
720
|
}
|
|
429
721
|
|
|
430
722
|
/** Create a SharedRef proxy backed by a SharedMemory instance. */
|
|
@@ -444,6 +736,26 @@ function createSharedRef<T>(
|
|
|
444
736
|
// the version check in the get handler.
|
|
445
737
|
__cache: undefined as
|
|
446
738
|
{ version: number; value: unknown; index: number } | undefined,
|
|
739
|
+
// Cached DataTable offset for this key, valid only while the global
|
|
740
|
+
// layout version is unchanged (i.e. no defragmentation has moved it).
|
|
741
|
+
// Mirrors the module-level offsetCache to avoid a Map lookup on the
|
|
742
|
+
// write hot path.
|
|
743
|
+
__offset: undefined as number | undefined,
|
|
744
|
+
__layoutVersion: -1,
|
|
745
|
+
// Cached slot type for this key, so the write hot path can skip the
|
|
746
|
+
// DataTable type read when the value's type is known to be stable.
|
|
747
|
+
__type: undefined as number | undefined,
|
|
748
|
+
// Cached ArrayRef for a top-level array value, so the write hot path can
|
|
749
|
+
// skip re-reading the 3 header Int32s on every index write. Valid only
|
|
750
|
+
// while the global layout version is unchanged (no defragmentation) and
|
|
751
|
+
// while the array has not grown past the cached count (checked by the
|
|
752
|
+
// caller via `index < ref.count`).
|
|
753
|
+
__arrayRef: undefined as
|
|
754
|
+
{ offset: number; count: number; capacity: number } | undefined,
|
|
755
|
+
__arrayRefLayoutVersion: -1,
|
|
756
|
+
// Cached version-cell index for this key, so the write hot path can bump
|
|
757
|
+
// the version cell without a Map lookup. Stable for the key's lifetime.
|
|
758
|
+
__index: undefined as number | undefined,
|
|
447
759
|
read: () => {
|
|
448
760
|
const value = copyValue(readValue(mem, key, target)) as T;
|
|
449
761
|
if (tracker && isTrackable(value)) {
|
|
@@ -454,7 +766,7 @@ function createSharedRef<T>(
|
|
|
454
766
|
},
|
|
455
767
|
write: (value: T) => {
|
|
456
768
|
target.__cache = undefined;
|
|
457
|
-
writeValue(mem, key, value, arrayCapacity);
|
|
769
|
+
writeValue(mem, key, value, arrayCapacity, target);
|
|
458
770
|
},
|
|
459
771
|
update: (fn: (current: T) => T) => {
|
|
460
772
|
target.__cache = undefined;
|
|
@@ -521,33 +833,106 @@ function createSharedRef<T>(
|
|
|
521
833
|
},
|
|
522
834
|
});
|
|
523
835
|
const ref = Object.create(proto) as Record<string | symbol, unknown>;
|
|
836
|
+
// Resolve the object's stable version-cell index lazily (the value may not
|
|
837
|
+
// be written to shared memory until after this accessor is built) and cache
|
|
838
|
+
// it so the hot getter can load the version cell directly without a Map
|
|
839
|
+
// lookup or an index→cell computation on every read. The cell index is
|
|
840
|
+
// stable for the key's lifetime (the Index Table is never reordered).
|
|
841
|
+
let cellIndex: number | null = null;
|
|
524
842
|
for (const name of propNames) {
|
|
843
|
+
// Per-property cache slot bound to this getter closure (mirrors
|
|
844
|
+
// cofusion's per-field `desc.cachedVer`/`desc.cachedVal`). On a cache
|
|
845
|
+
// hit the getter returns the already-decoded value directly, avoiding
|
|
846
|
+
// the `readCache` Map hash lookup and the whole-object `v[name]`
|
|
847
|
+
// extraction on the hot path.
|
|
848
|
+
const fieldCache: { version: number; value: unknown } = {
|
|
849
|
+
version: -1,
|
|
850
|
+
value: undefined,
|
|
851
|
+
};
|
|
852
|
+
// Stable array proxy for this property, cached across writes. A write
|
|
853
|
+
// bumps the version and invalidates `fieldCache`, which would otherwise
|
|
854
|
+
// recreate the array proxy on every access. The proxy reads fresh data
|
|
855
|
+
// on each access, so it stays correct without being recreated.
|
|
856
|
+
let arrayProxy: unknown[] | undefined;
|
|
857
|
+
// Cached object slot offset for this property so writes skip the
|
|
858
|
+
// getObjectLayout Map lookup + properties.get on the hot path.
|
|
859
|
+
// Invalidated when the layout version changes (defragmentation moves
|
|
860
|
+
// the object block).
|
|
861
|
+
let slotOffset: number | undefined;
|
|
862
|
+
let slotLayoutVersion = -1;
|
|
525
863
|
Object.defineProperty(ref, name, {
|
|
526
864
|
enumerable: true,
|
|
527
865
|
configurable: false,
|
|
528
866
|
get() {
|
|
529
867
|
// Lock-free fast path inlined so the JIT can optimize the hot
|
|
530
868
|
// cache-hit read without a function-call chain.
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
) {
|
|
536
|
-
|
|
537
|
-
if (Array.isArray(v)) {
|
|
538
|
-
return getTopLevelArrayMember(mem, key, v, name, arrayCapacity);
|
|
539
|
-
}
|
|
540
|
-
const pv = (v as Record<string | symbol, unknown>)[name];
|
|
541
|
-
if (Array.isArray(pv)) {
|
|
542
|
-
return createArrayProxy(mem, key, name, arrayCapacity);
|
|
543
|
-
}
|
|
544
|
-
return pv;
|
|
869
|
+
if (cellIndex === null) {
|
|
870
|
+
cellIndex = mem.getVersionCellIndex(getIndex(mem, key));
|
|
871
|
+
}
|
|
872
|
+
const version = Atomics.load(mem.versionView, cellIndex);
|
|
873
|
+
if (fieldCache.version === version) {
|
|
874
|
+
return fieldCache.value;
|
|
545
875
|
}
|
|
546
|
-
|
|
876
|
+
// If this property is an array, return the stable cached proxy
|
|
877
|
+
// (which reads fresh data on each access) instead of recreating it
|
|
878
|
+
// on every write.
|
|
879
|
+
if (arrayProxy !== undefined) {
|
|
880
|
+
return arrayProxy;
|
|
881
|
+
}
|
|
882
|
+
const value = readObjectProp(
|
|
883
|
+
mem,
|
|
884
|
+
key,
|
|
885
|
+
name,
|
|
886
|
+
fieldCache,
|
|
887
|
+
version,
|
|
888
|
+
arrayCapacity,
|
|
889
|
+
);
|
|
890
|
+
if (Array.isArray(value)) {
|
|
891
|
+
arrayProxy = value;
|
|
892
|
+
}
|
|
893
|
+
return value;
|
|
547
894
|
},
|
|
548
895
|
set(v: unknown) {
|
|
549
896
|
target.__cache = undefined;
|
|
550
|
-
|
|
897
|
+
mem.beginWrite();
|
|
898
|
+
if (mem.isDefragmenting()) {
|
|
899
|
+
// A defrag pass may move the object block: drop the cached slot
|
|
900
|
+
// offset so it is re-resolved, and run the write under the mutex.
|
|
901
|
+
mem.endWrite();
|
|
902
|
+
slotOffset = undefined;
|
|
903
|
+
slotLayoutVersion = -1;
|
|
904
|
+
mem.withLock(() =>
|
|
905
|
+
writePropInner(mem, key, name, v, arrayCapacity, target),
|
|
906
|
+
);
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
try {
|
|
910
|
+
// Write directly to the cached slot offset when still valid,
|
|
911
|
+
// skipping the getObjectLayout Map lookup + properties.get.
|
|
912
|
+
if (slotLayoutVersion !== mem.getLayoutVersion()) {
|
|
913
|
+
slotOffset = undefined;
|
|
914
|
+
slotLayoutVersion = mem.getLayoutVersion();
|
|
915
|
+
}
|
|
916
|
+
if (slotOffset === undefined) {
|
|
917
|
+
const layout = getObjectLayout(mem, key);
|
|
918
|
+
if (layout === null) {
|
|
919
|
+
// Not an object (shouldn't happen for an object accessor).
|
|
920
|
+
throw new TypeError(
|
|
921
|
+
`Value for key "${key}" has a fixed shape: properties may not be added or removed`,
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
const so = layout.properties.get(name);
|
|
925
|
+
if (so === undefined) {
|
|
926
|
+
throw new TypeError(
|
|
927
|
+
`Value for key "${key}" has a fixed shape: properties may not be added or removed`,
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
slotOffset = so;
|
|
931
|
+
}
|
|
932
|
+
writeObjectSlot(mem, key, slotOffset, v, arrayCapacity, target);
|
|
933
|
+
} finally {
|
|
934
|
+
mem.endWrite();
|
|
935
|
+
}
|
|
551
936
|
},
|
|
552
937
|
});
|
|
553
938
|
}
|
|
@@ -581,7 +966,7 @@ function createSharedRef<T>(
|
|
|
581
966
|
},
|
|
582
967
|
set(t, prop, val) {
|
|
583
968
|
t.__cache = undefined;
|
|
584
|
-
writeProp(mem, key, prop, val, arrayCapacity);
|
|
969
|
+
writeProp(mem, key, prop, val, arrayCapacity, t);
|
|
585
970
|
return true;
|
|
586
971
|
},
|
|
587
972
|
deleteProperty() {
|
|
@@ -592,22 +977,6 @@ function createSharedRef<T>(
|
|
|
592
977
|
}) as unknown as SharedRef<T>;
|
|
593
978
|
}
|
|
594
979
|
|
|
595
|
-
/**
|
|
596
|
-
* Lock-free fast path: if the cached version matches the current version cell,
|
|
597
|
-
* the cached value is still valid. Returns the cached value, or a miss marker.
|
|
598
|
-
* Kept small so the JIT can inline it into hot accessor getters.
|
|
599
|
-
*/
|
|
600
|
-
function readCachedValue(
|
|
601
|
-
mem: SharedMemory,
|
|
602
|
-
key: string,
|
|
603
|
-
): { hit: true; value: unknown } | { hit: false } {
|
|
604
|
-
const cached = readCache.get(key);
|
|
605
|
-
if (cached !== undefined && cached.version === mem.getVersion(cached.index)) {
|
|
606
|
-
return { hit: true, value: cached.value };
|
|
607
|
-
}
|
|
608
|
-
return { hit: false };
|
|
609
|
-
}
|
|
610
|
-
|
|
611
980
|
/**
|
|
612
981
|
* Read the full value for a key from shared memory, using the read cache.
|
|
613
982
|
*
|
|
@@ -622,10 +991,13 @@ function readValue(
|
|
|
622
991
|
__cache?: { version: number; value: unknown; index: number };
|
|
623
992
|
},
|
|
624
993
|
): unknown {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
994
|
+
// Inline the read-cache fast path (see readProp) to avoid the polymorphic
|
|
995
|
+
// {hit,value} union returned by readCachedValue.
|
|
628
996
|
const cached = readCache.get(key);
|
|
997
|
+
if (cached !== undefined && cached.version === mem.getVersion(cached.index)) {
|
|
998
|
+
return cached.value;
|
|
999
|
+
}
|
|
1000
|
+
|
|
629
1001
|
const index = cached !== undefined ? cached.index : getIndex(mem, key);
|
|
630
1002
|
const version = mem.getVersion(index);
|
|
631
1003
|
|
|
@@ -661,61 +1033,156 @@ function writeValue(
|
|
|
661
1033
|
key: string,
|
|
662
1034
|
value: unknown,
|
|
663
1035
|
arrayCapacity?: number,
|
|
1036
|
+
target?: WriteTarget,
|
|
664
1037
|
): void {
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
1038
|
+
// Inline writeLock to avoid a closure allocation on the hot path.
|
|
1039
|
+
mem.beginWrite();
|
|
1040
|
+
if (mem.isDefragmenting()) {
|
|
1041
|
+
mem.endWrite();
|
|
1042
|
+
mem.withLock(() => writeValueInner(mem, key, value, arrayCapacity, target));
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
try {
|
|
1046
|
+
writeValueInner(mem, key, value, arrayCapacity, target);
|
|
1047
|
+
} finally {
|
|
1048
|
+
mem.endWrite();
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function writeValueInner(
|
|
1053
|
+
mem: SharedMemory,
|
|
1054
|
+
key: string,
|
|
1055
|
+
value: unknown,
|
|
1056
|
+
arrayCapacity?: number,
|
|
1057
|
+
target?: WriteTarget,
|
|
1058
|
+
): void {
|
|
1059
|
+
const existing = getCachedOffset(mem, key, target);
|
|
1060
|
+
if (existing !== null) {
|
|
1061
|
+
// Skip the DataTable type read when the slot type is known to be stable
|
|
1062
|
+
// (cached on the target). Only re-read when the type is unknown.
|
|
1063
|
+
let type = target?.__type;
|
|
1064
|
+
if (type === undefined) {
|
|
1065
|
+
type = mem.dataTable.getInt32(existing);
|
|
1066
|
+
if (target !== undefined) target.__type = type;
|
|
1067
|
+
}
|
|
1068
|
+
if (type === 5) {
|
|
1069
|
+
// Update existing object's property slots in place.
|
|
1070
|
+
const properties = mem.readObject(existing);
|
|
1071
|
+
const valueObj = value as Record<string, unknown>;
|
|
1072
|
+
for (let i = 0; i < properties.length; i++) {
|
|
1073
|
+
const [name] = properties[i]!;
|
|
1074
|
+
if (!(name in valueObj)) {
|
|
1075
|
+
throw new TypeError(
|
|
1076
|
+
`Value for key "${key}" has a fixed shape: properties may not be added or removed`,
|
|
684
1077
|
);
|
|
685
1078
|
}
|
|
686
|
-
//
|
|
687
|
-
const
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
1079
|
+
// Free the old referenced blocks (array/string) before overwriting.
|
|
1080
|
+
const oldPropSlot = properties[i]![1];
|
|
1081
|
+
freeSlotRefs(mem, oldPropSlot);
|
|
1082
|
+
mem.updateObjectSlot(
|
|
1083
|
+
existing,
|
|
1084
|
+
i,
|
|
1085
|
+
encodeValue(mem, valueObj[name], arrayCapacity, oldPropSlot),
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
1088
|
+
// Check for extra properties.
|
|
1089
|
+
const existingNames = new Set(properties.map(([n]) => n));
|
|
1090
|
+
for (const name of Object.keys(valueObj)) {
|
|
1091
|
+
if (!existingNames.has(name)) {
|
|
1092
|
+
throw new TypeError(
|
|
1093
|
+
`Value for key "${key}" has a fixed shape: properties may not be added or removed`,
|
|
1094
|
+
);
|
|
694
1095
|
}
|
|
695
|
-
bump(mem, key);
|
|
696
|
-
return;
|
|
697
1096
|
}
|
|
698
|
-
|
|
699
|
-
mem.updateValueSlot(existing, encodeValue(mem, value, arrayCapacity));
|
|
700
|
-
bump(mem, key);
|
|
1097
|
+
bumpTarget(mem, key, target);
|
|
701
1098
|
return;
|
|
702
1099
|
}
|
|
703
|
-
|
|
704
|
-
if (
|
|
705
|
-
//
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
1100
|
+
// Update a single value slot in place.
|
|
1101
|
+
if (typeof value === "number") {
|
|
1102
|
+
// Fast path: numbers need no old-slot read (freeSlotRefs is a no-op)
|
|
1103
|
+
// and no in-place string reuse; write the float64 directly.
|
|
1104
|
+
mem.updateNumberSlot(existing, value);
|
|
1105
|
+
bumpTarget(mem, key, target);
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
const isPrimitive = typeof value === "boolean" || typeof value === "bigint";
|
|
1109
|
+
if (isPrimitive) {
|
|
1110
|
+
// Fast path: primitives need no old-slot read (freeSlotRefs is a
|
|
1111
|
+
// no-op) and no in-place string reuse.
|
|
1112
|
+
if (typeof value === "boolean") {
|
|
1113
|
+
mem.updateBooleanSlot(existing, value);
|
|
1114
|
+
} else if (value >= MIN_INT64 && value <= MAX_INT64) {
|
|
1115
|
+
// Fast path: int64 bigints are written directly, avoiding the
|
|
1116
|
+
// ValueSlot object allocation of the generic path.
|
|
1117
|
+
mem.updateBigIntSlot(existing, value);
|
|
1118
|
+
} else {
|
|
1119
|
+
mem.updateValueSlot(existing, encodeValue(mem, value, arrayCapacity));
|
|
1120
|
+
}
|
|
1121
|
+
bumpTarget(mem, key, target);
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
if (typeof value === "string") {
|
|
1125
|
+
// Fast path: short strings are written inline into the slot itself,
|
|
1126
|
+
// avoiding a heap allocation and the string-table write. Longer strings
|
|
1127
|
+
// read the old string ref directly (no ValueSlot object allocation) so
|
|
1128
|
+
// storeStringValue can reuse the block in place.
|
|
1129
|
+
if (mem.updateInlineStringSlot(existing, value)) {
|
|
1130
|
+
bumpTarget(mem, key, target);
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
const oldType = mem.dataTable.getInt32(existing);
|
|
1134
|
+
const oldA = mem.dataTable.getInt32(existing + 4);
|
|
1135
|
+
const oldB = mem.dataTable.getInt32(existing + 8);
|
|
1136
|
+
const ref = mem.storeStringValue(
|
|
1137
|
+
value,
|
|
1138
|
+
oldType === TYPE_STRING_VALUE_REF
|
|
1139
|
+
? { offset: oldA, length: oldB }
|
|
1140
|
+
: undefined,
|
|
709
1141
|
);
|
|
710
|
-
|
|
711
|
-
mem
|
|
1142
|
+
mem.updateValueSlot(existing, stringValueRefSlot(ref));
|
|
1143
|
+
bumpTarget(mem, key, target);
|
|
712
1144
|
return;
|
|
713
1145
|
}
|
|
1146
|
+
const oldSlot = mem.readValueSlot(existing);
|
|
1147
|
+
freeSlotRefs(mem, oldSlot);
|
|
1148
|
+
mem.updateValueSlot(
|
|
1149
|
+
existing,
|
|
1150
|
+
encodeValue(mem, value, arrayCapacity, oldSlot),
|
|
1151
|
+
);
|
|
1152
|
+
bumpTarget(mem, key, target);
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
714
1155
|
|
|
715
|
-
|
|
716
|
-
|
|
1156
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
1157
|
+
// Create a new object.
|
|
1158
|
+
const valueObj = value as Record<string, unknown>;
|
|
1159
|
+
const props: Array<[string, ValueSlot]> = Object.entries(valueObj).map(
|
|
1160
|
+
([name, v]) => [name, encodeValue(mem, v, arrayCapacity)],
|
|
1161
|
+
);
|
|
1162
|
+
const offset = mem.createObject(props);
|
|
717
1163
|
mem.insert(key, offset);
|
|
718
|
-
|
|
1164
|
+
const lv = mem.getLayoutVersion();
|
|
1165
|
+
offsetCache.set(key, { offset, layoutVersion: lv });
|
|
1166
|
+
if (target !== undefined) {
|
|
1167
|
+
target.__offset = offset;
|
|
1168
|
+
target.__layoutVersion = lv;
|
|
1169
|
+
target.__type = undefined;
|
|
1170
|
+
target.__arrayRef = undefined;
|
|
1171
|
+
}
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// Create a single value slot (primitive or array).
|
|
1176
|
+
const offset = mem.storeValueSlot(encodeValue(mem, value, arrayCapacity));
|
|
1177
|
+
mem.insert(key, offset);
|
|
1178
|
+
const lv = mem.getLayoutVersion();
|
|
1179
|
+
offsetCache.set(key, { offset, layoutVersion: lv });
|
|
1180
|
+
if (target !== undefined) {
|
|
1181
|
+
target.__offset = offset;
|
|
1182
|
+
target.__layoutVersion = lv;
|
|
1183
|
+
target.__type = undefined;
|
|
1184
|
+
target.__arrayRef = undefined;
|
|
1185
|
+
}
|
|
719
1186
|
}
|
|
720
1187
|
|
|
721
1188
|
/** Bump the version cell for a key, invalidating read caches. */
|
|
@@ -723,6 +1190,25 @@ function bump(mem: SharedMemory, key: string): void {
|
|
|
723
1190
|
mem.bumpVersion(getIndex(mem, key));
|
|
724
1191
|
}
|
|
725
1192
|
|
|
1193
|
+
/**
|
|
1194
|
+
* Bump the version cell for a key using the target's cached index, avoiding
|
|
1195
|
+
* the `getIndex` Map lookup on the write hot path. Falls back to `bump` when
|
|
1196
|
+
* the index is not yet cached.
|
|
1197
|
+
*/
|
|
1198
|
+
function bumpTarget(
|
|
1199
|
+
mem: SharedMemory,
|
|
1200
|
+
key: string,
|
|
1201
|
+
target?: WriteTarget,
|
|
1202
|
+
): void {
|
|
1203
|
+
if (target !== undefined && target.__index !== undefined) {
|
|
1204
|
+
mem.bumpVersion(target.__index);
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
const index = getIndex(mem, key);
|
|
1208
|
+
if (target !== undefined) target.__index = index;
|
|
1209
|
+
mem.bumpVersion(index);
|
|
1210
|
+
}
|
|
1211
|
+
|
|
726
1212
|
/** Atomically update the value for a key. */
|
|
727
1213
|
function updateValue<T>(
|
|
728
1214
|
mem: SharedMemory,
|
|
@@ -730,7 +1216,7 @@ function updateValue<T>(
|
|
|
730
1216
|
fn: (current: T) => T,
|
|
731
1217
|
arrayCapacity?: number,
|
|
732
1218
|
): T {
|
|
733
|
-
return mem
|
|
1219
|
+
return writeLock(mem, () => {
|
|
734
1220
|
const current = readValue(mem, key) as T;
|
|
735
1221
|
const next = fn(current);
|
|
736
1222
|
writeValue(mem, key, next, arrayCapacity);
|
|
@@ -749,7 +1235,7 @@ function mutateValue<T, R>(
|
|
|
749
1235
|
fn: (current: T) => R,
|
|
750
1236
|
arrayCapacity?: number,
|
|
751
1237
|
): R {
|
|
752
|
-
return mem
|
|
1238
|
+
return writeLock(mem, () => {
|
|
753
1239
|
const current = readValue(mem, key) as T;
|
|
754
1240
|
const result = fn(current);
|
|
755
1241
|
writeValue(mem, key, current, arrayCapacity);
|
|
@@ -794,7 +1280,7 @@ function mutateArrayRef(
|
|
|
794
1280
|
method: "push" | "pop",
|
|
795
1281
|
args: unknown[],
|
|
796
1282
|
): unknown {
|
|
797
|
-
return mem
|
|
1283
|
+
return writeLock(mem, () => {
|
|
798
1284
|
const slotOffset = findArrayRefSlot(mem, key, prop);
|
|
799
1285
|
if (slotOffset === null) {
|
|
800
1286
|
throw new TypeError("Value is not an array");
|
|
@@ -822,6 +1308,8 @@ function mutateArrayRef(
|
|
|
822
1308
|
// Read the last element before decrementing.
|
|
823
1309
|
const lastSlot = mem.readArraySlot(ref, ref.count - 1);
|
|
824
1310
|
const result = decodeValue(mem, lastSlot);
|
|
1311
|
+
// Free the removed element's referenced blocks (array/string).
|
|
1312
|
+
freeSlotRefs(mem, lastSlot);
|
|
825
1313
|
const next = {
|
|
826
1314
|
offset: ref.offset,
|
|
827
1315
|
count: ref.count - 1,
|
|
@@ -883,6 +1371,159 @@ function createArrayProxy(
|
|
|
883
1371
|
prop: string,
|
|
884
1372
|
arrayCapacity?: number,
|
|
885
1373
|
): unknown[] {
|
|
1374
|
+
// Cache the object's property slot offset and the array ref so index writes
|
|
1375
|
+
// skip the per-write getObjectLayout Map lookup, properties.get, and the
|
|
1376
|
+
// DataTable header reads. Invalidated when the layout version changes
|
|
1377
|
+
// (defragmentation or array growth via moveArray).
|
|
1378
|
+
let cachedSlotOffset: number | undefined;
|
|
1379
|
+
let cachedRef:
|
|
1380
|
+
{ offset: number; count: number; capacity: number } | undefined;
|
|
1381
|
+
let cachedLayoutVersion = -1;
|
|
1382
|
+
// Cached version-cell index for this key, so writes can bump the version
|
|
1383
|
+
// cell without a Map lookup on every write. Stable for the key's lifetime.
|
|
1384
|
+
let cachedIndex: number | undefined;
|
|
1385
|
+
|
|
1386
|
+
/** Bump the key's version cell using the cached index (resolved lazily). */
|
|
1387
|
+
function bumpCached(): void {
|
|
1388
|
+
if (cachedIndex === undefined) {
|
|
1389
|
+
cachedIndex = getIndex(mem, key);
|
|
1390
|
+
}
|
|
1391
|
+
mem.bumpVersion(cachedIndex);
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
/**
|
|
1395
|
+
* Write a single array-property slot in place. Defined once (not per call)
|
|
1396
|
+
* so the set handler can inline the writeLock without allocating a closure
|
|
1397
|
+
* on every write. Mutates the cached slot offset/ref/layout version.
|
|
1398
|
+
*/
|
|
1399
|
+
function writeArrayProp(arrProp: string | symbol, val: unknown): boolean {
|
|
1400
|
+
// Refresh the cached slot offset when the layout version changed
|
|
1401
|
+
// (defragmentation may have moved the object block).
|
|
1402
|
+
if (cachedLayoutVersion !== mem.getLayoutVersion()) {
|
|
1403
|
+
cachedSlotOffset = undefined;
|
|
1404
|
+
cachedRef = undefined;
|
|
1405
|
+
cachedLayoutVersion = mem.getLayoutVersion();
|
|
1406
|
+
}
|
|
1407
|
+
if (cachedSlotOffset === undefined) {
|
|
1408
|
+
const layout = getObjectLayout(mem, key);
|
|
1409
|
+
if (layout === null) {
|
|
1410
|
+
// Fallback: full read-modify-write.
|
|
1411
|
+
return mutateValue<Record<string, unknown>, boolean>(
|
|
1412
|
+
mem,
|
|
1413
|
+
key,
|
|
1414
|
+
(fresh) => {
|
|
1415
|
+
const arr = fresh[prop] as unknown[];
|
|
1416
|
+
(arr as unknown as Record<string | symbol, unknown>)[arrProp] = val;
|
|
1417
|
+
return true;
|
|
1418
|
+
},
|
|
1419
|
+
arrayCapacity,
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
const so = layout.properties.get(prop);
|
|
1423
|
+
if (so === undefined) {
|
|
1424
|
+
throw new TypeError(
|
|
1425
|
+
`Value for key "${key}" has a fixed shape: properties may not be added or removed`,
|
|
1426
|
+
);
|
|
1427
|
+
}
|
|
1428
|
+
cachedSlotOffset = so;
|
|
1429
|
+
}
|
|
1430
|
+
const propSlot = mem.readValueSlot(cachedSlotOffset);
|
|
1431
|
+
if (
|
|
1432
|
+
propSlot.type === TYPE_ARRAY_REF &&
|
|
1433
|
+
typeof arrProp === "string" &&
|
|
1434
|
+
/^\d+$/.test(arrProp)
|
|
1435
|
+
) {
|
|
1436
|
+
const index = Number(arrProp);
|
|
1437
|
+
// Use the cached ref when valid; re-read when the array may have
|
|
1438
|
+
// grown past the cached count (push) or the layout changed.
|
|
1439
|
+
let ref = cachedRef;
|
|
1440
|
+
if (
|
|
1441
|
+
ref === undefined ||
|
|
1442
|
+
index >= ref.count ||
|
|
1443
|
+
cachedLayoutVersion !== mem.getLayoutVersion()
|
|
1444
|
+
) {
|
|
1445
|
+
ref = {
|
|
1446
|
+
offset: propSlot.a,
|
|
1447
|
+
count: propSlot.b,
|
|
1448
|
+
capacity: propSlot.c,
|
|
1449
|
+
};
|
|
1450
|
+
cachedRef = ref;
|
|
1451
|
+
}
|
|
1452
|
+
if (index >= ref.count) {
|
|
1453
|
+
throw new RangeError(
|
|
1454
|
+
`Index ${index} out of bounds for array of length ${ref.count}`,
|
|
1455
|
+
);
|
|
1456
|
+
}
|
|
1457
|
+
if (typeof val === "number") {
|
|
1458
|
+
// Fast path: numbers need no old-slot read (freeSlotRefs is a no-op).
|
|
1459
|
+
mem.updateArrayNumberSlot(ref, index, val);
|
|
1460
|
+
bumpCached();
|
|
1461
|
+
return true;
|
|
1462
|
+
}
|
|
1463
|
+
if (typeof val === "boolean") {
|
|
1464
|
+
// Fast path: booleans need no old-slot read (freeSlotRefs is a no-op).
|
|
1465
|
+
mem.updateArrayBooleanSlot(ref, index, val);
|
|
1466
|
+
bumpCached();
|
|
1467
|
+
return true;
|
|
1468
|
+
}
|
|
1469
|
+
if (typeof val === "bigint" && val >= MIN_INT64 && val <= MAX_INT64) {
|
|
1470
|
+
// Fast path: int64 bigints are written directly, avoiding the
|
|
1471
|
+
// ValueSlot object allocation of the generic path.
|
|
1472
|
+
mem.updateArrayBigIntSlot(ref, index, val);
|
|
1473
|
+
bumpCached();
|
|
1474
|
+
return true;
|
|
1475
|
+
}
|
|
1476
|
+
if (typeof val === "string") {
|
|
1477
|
+
// Fast path: short strings are written inline into the slot.
|
|
1478
|
+
if (mem.updateArrayInlineStringSlot(ref, index, val)) {
|
|
1479
|
+
bumpCached();
|
|
1480
|
+
return true;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
freeSlotRefs(mem, mem.readArraySlot(ref, index));
|
|
1484
|
+
mem.updateArraySlot(ref, index, encodeValue(mem, val));
|
|
1485
|
+
bumpCached();
|
|
1486
|
+
return true;
|
|
1487
|
+
}
|
|
1488
|
+
// Array property length write: shrink/grow in place without re-encoding
|
|
1489
|
+
// the whole object (which would leave a dead slot).
|
|
1490
|
+
if (propSlot.type === TYPE_ARRAY_REF && arrProp === "length") {
|
|
1491
|
+
const newLen = Number(val);
|
|
1492
|
+
if (!Number.isInteger(newLen) || newLen < 0) {
|
|
1493
|
+
throw new RangeError("Invalid array length");
|
|
1494
|
+
}
|
|
1495
|
+
const ref = {
|
|
1496
|
+
offset: propSlot.a,
|
|
1497
|
+
count: propSlot.b,
|
|
1498
|
+
capacity: propSlot.c,
|
|
1499
|
+
};
|
|
1500
|
+
if (newLen <= ref.capacity) {
|
|
1501
|
+
mem.updateValueSlot(
|
|
1502
|
+
cachedSlotOffset,
|
|
1503
|
+
arrayRefSlot({
|
|
1504
|
+
offset: ref.offset,
|
|
1505
|
+
count: newLen,
|
|
1506
|
+
capacity: ref.capacity,
|
|
1507
|
+
}),
|
|
1508
|
+
);
|
|
1509
|
+
bumpCached();
|
|
1510
|
+
return true;
|
|
1511
|
+
}
|
|
1512
|
+
// Growing beyond capacity falls through to the full read-modify-write.
|
|
1513
|
+
}
|
|
1514
|
+
// Fallback: full read-modify-write.
|
|
1515
|
+
return mutateValue<Record<string, unknown>, boolean>(
|
|
1516
|
+
mem,
|
|
1517
|
+
key,
|
|
1518
|
+
(fresh) => {
|
|
1519
|
+
const arr = fresh[prop] as unknown[];
|
|
1520
|
+
(arr as unknown as Record<string | symbol, unknown>)[arrProp] = val;
|
|
1521
|
+
return true;
|
|
1522
|
+
},
|
|
1523
|
+
arrayCapacity,
|
|
1524
|
+
);
|
|
1525
|
+
}
|
|
1526
|
+
|
|
886
1527
|
return new Proxy([] as unknown[], {
|
|
887
1528
|
get(_target, arrProp) {
|
|
888
1529
|
// push/pop on an array property: operate directly on the array ref
|
|
@@ -939,73 +1580,18 @@ function createArrayProxy(
|
|
|
939
1580
|
},
|
|
940
1581
|
set(_target, arrProp, val) {
|
|
941
1582
|
// In-place update: write only the affected array slot instead of
|
|
942
|
-
// decoding and re-encoding the whole object.
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
const index = Number(arrProp);
|
|
955
|
-
const ref = {
|
|
956
|
-
offset: propSlot.a,
|
|
957
|
-
count: propSlot.b,
|
|
958
|
-
capacity: propSlot.c,
|
|
959
|
-
};
|
|
960
|
-
if (index >= ref.count) {
|
|
961
|
-
throw new RangeError(
|
|
962
|
-
`Index ${index} out of bounds for array of length ${ref.count}`,
|
|
963
|
-
);
|
|
964
|
-
}
|
|
965
|
-
mem.updateArraySlot(ref, index, encodeValue(mem, val));
|
|
966
|
-
bump(mem, key);
|
|
967
|
-
return true;
|
|
968
|
-
}
|
|
969
|
-
// Array property length write: shrink/grow in place without
|
|
970
|
-
// re-encoding the whole object (which would leave a dead slot).
|
|
971
|
-
if (propSlot.type === TYPE_ARRAY_REF && arrProp === "length") {
|
|
972
|
-
const newLen = Number(val);
|
|
973
|
-
if (!Number.isInteger(newLen) || newLen < 0) {
|
|
974
|
-
throw new RangeError("Invalid array length");
|
|
975
|
-
}
|
|
976
|
-
const ref = {
|
|
977
|
-
offset: propSlot.a,
|
|
978
|
-
count: propSlot.b,
|
|
979
|
-
capacity: propSlot.c,
|
|
980
|
-
};
|
|
981
|
-
if (newLen <= ref.capacity) {
|
|
982
|
-
mem.updateValueSlot(
|
|
983
|
-
slotOffset,
|
|
984
|
-
arrayRefSlot({
|
|
985
|
-
offset: ref.offset,
|
|
986
|
-
count: newLen,
|
|
987
|
-
capacity: ref.capacity,
|
|
988
|
-
}),
|
|
989
|
-
);
|
|
990
|
-
bump(mem, key);
|
|
991
|
-
return true;
|
|
992
|
-
}
|
|
993
|
-
// Growing beyond capacity falls through to the full read-modify-write.
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
// Fallback: full read-modify-write.
|
|
998
|
-
return mutateValue<Record<string, unknown>, boolean>(
|
|
999
|
-
mem,
|
|
1000
|
-
key,
|
|
1001
|
-
(fresh) => {
|
|
1002
|
-
const arr = fresh[prop] as unknown[];
|
|
1003
|
-
(arr as unknown as Record<string | symbol, unknown>)[arrProp] = val;
|
|
1004
|
-
return true;
|
|
1005
|
-
},
|
|
1006
|
-
arrayCapacity,
|
|
1007
|
-
);
|
|
1008
|
-
});
|
|
1583
|
+
// decoding and re-encoding the whole object. Inline writeLock to avoid
|
|
1584
|
+
// a closure allocation on the hot path.
|
|
1585
|
+
mem.beginWrite();
|
|
1586
|
+
if (mem.isDefragmenting()) {
|
|
1587
|
+
mem.endWrite();
|
|
1588
|
+
return mem.withLock(() => writeArrayProp(arrProp, val));
|
|
1589
|
+
}
|
|
1590
|
+
try {
|
|
1591
|
+
return writeArrayProp(arrProp, val);
|
|
1592
|
+
} finally {
|
|
1593
|
+
mem.endWrite();
|
|
1594
|
+
}
|
|
1009
1595
|
},
|
|
1010
1596
|
});
|
|
1011
1597
|
}
|