@@ -60,6 +60,11 @@ export class ShareableMap<K, V> extends TransferableDataStructure {
6060 private indexView ! : DataView ;
6161 private dataView ! : DataView ;
6262
63+ // Cached Int32 view of the index buffer — used exclusively for Atomics lock operations.
64+ // The lock word sits at a fixed offset in the metadata region, so this view never needs
65+ // to be refreshed even after SharedArrayBuffer.grow() calls.
66+ private int32LockArray : Int32Array | null = null ;
67+
6368 private textDecoder : TextDecoder = new TextDecoder ( ) ;
6469
6570 private readonly stringEncoder = new StringEncoder ( ) ;
@@ -160,6 +165,7 @@ export class ShareableMap<K, V> extends TransferableDataStructure {
160165 protected setBuffers ( indexBuffer : SharedArrayBuffer | ArrayBuffer , dataBuffer : SharedArrayBuffer | ArrayBuffer ) {
161166 this . indexMem = indexBuffer ;
162167 this . indexView = new DataView ( this . indexMem ) ;
168+ this . int32LockArray = indexBuffer instanceof SharedArrayBuffer ? new Int32Array ( indexBuffer ) : null ;
163169 this . dataMem = dataBuffer ;
164170 this . dataView = new DataView ( this . dataMem ) ;
165171 }
@@ -384,28 +390,36 @@ export class ShareableMap<K, V> extends TransferableDataStructure {
384390 }
385391
386392 if ( needsToBeStored ) {
393+ // Read freeStart once into a local to avoid repeated getUint32 calls on the shared buffer.
394+ let freeStart = this . freeStart ;
395+
387396 // Determine if the data storage needs to be resized.
388- if ( maxKeyLength + maxValueLength + this . freeStart + ShareableMap . DATA_OBJECT_OFFSET > this . dataView . byteLength ) {
397+ if ( maxKeyLength + maxValueLength + freeStart + ShareableMap . DATA_OBJECT_OFFSET > this . dataView . byteLength ) {
389398 // We don't have enough space left at the end of the data array. We should now consider if we should just
390399 // perform a defragmentation of the data array, or if we need to double the size of the array.
391- const defragRatio = this . spaceUsedInDataPartition / this . dataView . byteLength ;
400+ const usedSpace = this . spaceUsedInDataPartition ;
401+ const defragRatio = usedSpace / this . dataView . byteLength ;
392402
393403 if (
394404 defragRatio < ShareableMap . MIN_DEFRAG_FACTOR &&
395- this . spaceUsedInDataPartition + maxKeyLength + maxValueLength + ShareableMap . DATA_OBJECT_OFFSET < this . dataView . byteLength
405+ usedSpace + maxKeyLength + maxValueLength + ShareableMap . DATA_OBJECT_OFFSET < this . dataView . byteLength
396406 ) {
397407 this . defragment ( ) ;
398408 } else {
399409 this . doubleDataStorage ( ) ;
400410 }
401411
412+ // Re-read freeStart: defragment() compacts and updates it; doubleDataStorage() may
413+ // have changed the DataView but freeStart stays the same (no re-read strictly needed
414+ // after grow, but we keep it uniform).
415+ freeStart = this . freeStart ;
402416 }
403417
404418 const exactKeyLength = this . stringEncoder . encode (
405419 keyString ,
406420 new Uint8Array (
407421 this . dataMem ,
408- ShareableMap . DATA_OBJECT_OFFSET + this . freeStart ,
422+ ShareableMap . DATA_OBJECT_OFFSET + freeStart ,
409423 maxKeyLength
410424 )
411425 ) ;
@@ -414,36 +428,38 @@ export class ShareableMap<K, V> extends TransferableDataStructure {
414428 value ,
415429 new Uint8Array (
416430 this . dataMem ,
417- ShareableMap . DATA_OBJECT_OFFSET + this . freeStart + exactKeyLength ,
431+ ShareableMap . DATA_OBJECT_OFFSET + freeStart + exactKeyLength ,
418432 maxValueLength
419433 )
420434 ) ;
421435
422436 // Store key length
423- this . dataView . setUint32 ( this . freeStart + 4 , exactKeyLength ) ;
437+ this . dataView . setUint32 ( freeStart + 4 , exactKeyLength ) ;
424438 // Store value length
425- this . dataView . setUint32 ( this . freeStart + 8 , exactValueLength ) ;
439+ this . dataView . setUint32 ( freeStart + 8 , exactValueLength ) ;
426440 // Keep track of key and value datatypes
427- this . dataView . setUint16 ( this . freeStart + 12 , typeof key === "string" ? 1 : 0 ) ;
428- this . dataView . setUint16 ( this . freeStart + 14 , valueEncoderId ) ;
429- this . dataView . setUint32 ( this . freeStart + 16 , hash ) ;
441+ this . dataView . setUint16 ( freeStart + 12 , typeof key === "string" ? 1 : 0 ) ;
442+ this . dataView . setUint16 ( freeStart + 14 , valueEncoderId ) ;
443+ this . dataView . setUint32 ( freeStart + 16 , hash ) ;
430444
431- this . spaceUsedInDataPartition += ShareableMap . DATA_OBJECT_OFFSET + exactKeyLength + exactValueLength ;
445+ const itemSize = ShareableMap . DATA_OBJECT_OFFSET + exactKeyLength + exactValueLength ;
446+ this . spaceUsedInDataPartition += itemSize ;
432447
433- startPos = this . freeStart ;
434- this . freeStart += ShareableMap . DATA_OBJECT_OFFSET + exactKeyLength + exactValueLength ;
448+ startPos = freeStart ;
449+ this . freeStart = freeStart + itemSize ;
435450
436451 // Increase size of the map since we added a new element.
437452 this . increaseSize ( ) ;
438453
439454 const bucketPointer = this . indexView . getUint32 ( bucket + ShareableMap . INDEX_TABLE_OFFSET ) ;
440455 if ( bucketPointer === 0 ) {
441456 this . incrementBucketsInUse ( ) ;
442- this . indexView . setUint32 ( bucket + ShareableMap . INDEX_TABLE_OFFSET , startPos ) ;
457+ // next pointer at startPos is already 0 (zero-initialised data region)
443458 } else {
444- // Update linked list pointers
445- this . updateLinkedPointer ( bucketPointer , startPos , this . dataView ) ;
459+ // Prepend: new item's next points to the current chain head — O(1) vs O(chain)
460+ this . dataView . setUint32 ( startPos , bucketPointer ) ;
446461 }
462+ this . indexView . setUint32 ( bucket + ShareableMap . INDEX_TABLE_OFFSET , startPos ) ;
447463
448464 // If the load factor exceeds the recommended value, we need to rehash the map to make sure performance stays
449465 // acceptable.
@@ -550,8 +566,9 @@ export class ShareableMap<K, V> extends TransferableDataStructure {
550566
551567 private computeHashAndBucket ( key : string ) : [ number , number ] {
552568 const hash : number = fast1a32 ( key ) ;
553- // Bucket in which this value should be stored.
554- const bucket = ( hash % this . buckets ) * ShareableMap . INT_SIZE ;
569+ // Bucket in which this value should be stored. Because the bucket count is always a
570+ // power of two, a bitwise AND mask is equivalent to modulo and avoids integer division.
571+ const bucket = ( hash & ( this . buckets - 1 ) ) * ShareableMap . INT_SIZE ;
555572 return [ hash , bucket ] ;
556573 }
557574
@@ -578,20 +595,13 @@ export class ShareableMap<K, V> extends TransferableDataStructure {
578595
579596 const totalLength = keyLength + valueLength + ShareableMap . DATA_OBJECT_OFFSET ;
580597
581- for ( let i = 0 ; i < totalLength ; i ++ ) {
582- newView . setUint8 ( newOffset + i , this . dataView . getUint8 ( dataPointer + i ) ) ;
583- }
584-
585- // Pointer to next block is zero
586- newView . setUint32 ( newOffset , 0 ) ;
598+ new Uint8Array ( newData , newOffset , totalLength )
599+ . set ( new Uint8Array ( this . dataMem , dataPointer , totalLength ) ) ;
587600
601+ // Prepend this item at the head of the bucket's chain in the new layout.
588602 const currentBucketLink = this . indexView . getUint32 ( ShareableMap . INDEX_TABLE_OFFSET + bucket * ShareableMap . INT_SIZE ) ;
589- if ( currentBucketLink === 0 ) {
590- this . indexView . setUint32 ( ShareableMap . INDEX_TABLE_OFFSET + bucket * ShareableMap . INT_SIZE , newOffset ) ;
591- } else {
592- // We need to follow the links from the first block here and update those.
593- this . updateLinkedPointer ( currentBucketLink , newOffset , newView ) ;
594- }
603+ newView . setUint32 ( newOffset , currentBucketLink ) ;
604+ this . indexView . setUint32 ( ShareableMap . INDEX_TABLE_OFFSET + bucket * ShareableMap . INT_SIZE , newOffset ) ;
595605
596606 newOffset += totalLength ;
597607 dataPointer = this . dataView . getUint32 ( dataPointer ) ;
@@ -651,21 +661,22 @@ export class ShareableMap<K, V> extends TransferableDataStructure {
651661 ) ;
652662 while ( startPos !== 0 ) {
653663 const hash = this . readHashFromDataObject ( startPos ) ;
654- const newBucket = hash % newBuckets ;
664+ const newBucket = hash & ( newBuckets - 1 ) ;
655665 const existing = tempView . getUint32 (
656666 ShareableMap . INDEX_TABLE_OFFSET + newBucket * ShareableMap . INT_SIZE
657667 ) ;
668+ const next = this . dataView . getUint32 ( startPos ) ;
658669 if ( existing === 0 ) {
659670 bucketsInUse ++ ;
660- tempView . setUint32 (
661- ShareableMap . INDEX_TABLE_OFFSET + newBucket * ShareableMap . INT_SIZE ,
662- startPos
663- ) ;
671+ this . dataView . setUint32 ( startPos , 0 ) ; // terminate chain
664672 } else {
665- this . updateLinkedPointer ( existing , startPos , this . dataView ) ;
673+ // Prepend: new item's next points to current head — O(1)
674+ this . dataView . setUint32 ( startPos , existing ) ;
666675 }
667- const next = this . dataView . getUint32 ( startPos ) ;
668- this . dataView . setUint32 ( startPos , 0 ) ;
676+ tempView . setUint32 (
677+ ShareableMap . INDEX_TABLE_OFFSET + newBucket * ShareableMap . INT_SIZE ,
678+ startPos
679+ ) ;
669680 startPos = next ;
670681 }
671682 }
@@ -828,14 +839,20 @@ export class ShareableMap<K, V> extends TransferableDataStructure {
828839 // Fourth set of 4 bytes keep tracks of the DataBuffer's length.
829840 // Fifth set of 4 bytes keeps track of the space that's being used in total (to track the defrag factor).
830841 // Rest of the index maps buckets onto their starting position in the data array.
831- const buckets = Math . ceil ( expectedSize / ShareableMap . LOAD_FACTOR )
842+ //
843+ // Round up to the next power of two so computeHashAndBucket can use a fast bitwise
844+ // AND mask instead of modulo. doubleIndexStorage always doubles, so subsequent counts
845+ // remain powers of two as well.
846+ const rawBuckets = Math . ceil ( expectedSize / ShareableMap . LOAD_FACTOR ) ;
847+ const buckets = rawBuckets <= 1 ? 1 : 2 ** Math . ceil ( Math . log2 ( rawBuckets ) ) ;
832848 const indexSize = 5 * 4 + buckets * ShareableMap . INT_SIZE ;
833849
834850 const maxDataBytes = this . originalOptions . maxBytes ?? ShareableMap . DEFAULT_MAX_DATA_BYTES ;
835851 const maxIndexBytes = ShareableMap . DEFAULT_MAX_INDEX_BYTES ;
836852
837853 this . indexMem = this . allocateMemory ( indexSize , maxIndexBytes ) ;
838854 this . indexView = new DataView ( this . indexMem ) ;
855+ this . int32LockArray = this . indexMem instanceof SharedArrayBuffer ? new Int32Array ( this . indexMem ) : null ;
839856
840857 // Free space starts from position 1 in the data array (instead of 0, which we use to indicate the end).
841858 this . indexView . setUint32 ( ShareableMap . INDEX_FREE_START_INDEX_OFFSET , ShareableMap . INITIAL_DATA_OFFSET ) ;
@@ -868,7 +885,7 @@ export class ShareableMap<K, V> extends TransferableDataStructure {
868885 return true ;
869886 }
870887
871- const int32Array = new Int32Array ( this . indexMem ) ;
888+ const int32Array = this . int32LockArray ! ;
872889 const lockIdx = ShareableMap . INDEX_LOCK_OFFSET / 4 ;
873890 const startTime = Date . now ( ) ;
874891
@@ -907,7 +924,7 @@ export class ShareableMap<K, V> extends TransferableDataStructure {
907924 return ;
908925 }
909926
910- const int32Array = new Int32Array ( this . indexMem ) ;
927+ const int32Array = this . int32LockArray ! ;
911928 const lockIdx = ShareableMap . INDEX_LOCK_OFFSET / 4 ;
912929
913930 // Decrement the reader count. Atomics.sub returns the value *before* subtraction,
@@ -935,7 +952,7 @@ export class ShareableMap<K, V> extends TransferableDataStructure {
935952 return true ;
936953 }
937954
938- const int32Array = new Int32Array ( this . indexMem ) ;
955+ const int32Array = this . int32LockArray ! ;
939956 const lockIdx = ShareableMap . INDEX_LOCK_OFFSET / 4 ;
940957 const startTime = Date . now ( ) ;
941958
@@ -973,7 +990,7 @@ export class ShareableMap<K, V> extends TransferableDataStructure {
973990 return ;
974991 }
975992
976- const int32Array = new Int32Array ( this . indexMem ) ;
993+ const int32Array = this . int32LockArray ! ;
977994 const lockIdx = ShareableMap . INDEX_LOCK_OFFSET / 4 ;
978995
979996 Atomics . store ( int32Array , lockIdx , 0 ) ;
@@ -984,9 +1001,8 @@ export class ShareableMap<K, V> extends TransferableDataStructure {
9841001 * Initialise the lock word to the unlocked state (0).
9851002 */
9861003 private initializeLockState ( ) : void {
987- if ( this . indexMem instanceof SharedArrayBuffer ) {
988- const int32Array = new Int32Array ( this . indexMem ) ;
989- Atomics . store ( int32Array , ShareableMap . INDEX_LOCK_OFFSET / 4 , 0 ) ;
1004+ if ( this . int32LockArray !== null ) {
1005+ Atomics . store ( this . int32LockArray , ShareableMap . INDEX_LOCK_OFFSET / 4 , 0 ) ;
9901006 }
9911007 }
9921008}
0 commit comments