diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..801947c --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,6 @@ +## 2026-01-19 - Avoid Intermediate Buffers with TextEncoder +**Learning:** TextEncoder.encodeInto works directly with Uint8Array views on SharedArrayBuffer. Intermediate buffers and copying are unnecessary and can double the execution time. +**Action:** Check TextEncoder usage for direct writing to destination buffers. +## 2026-01-20 - Optimize Read Performance with Zero-Copy +**Learning:** TextDecoder can sometimes decode directly from SharedArrayBuffer views, but browser support varies. Feature detection is required. NumberEncoder (DataView) always supports SAB but requires respecting byteOffset. +**Action:** Use feature detection for TextDecoder and fix DataView offsets to enable zero-copy reads. diff --git a/src/array/ShareableArray.ts b/src/array/ShareableArray.ts index dcea152..15e8eee 100644 --- a/src/array/ShareableArray.ts +++ b/src/array/ShareableArray.ts @@ -3,6 +3,7 @@ import {Serializable} from "../encoding"; import StringEncoder from "../encoding/StringEncoder"; import NumberEncoder from "../encoding/NumberEncoder"; import GeneralPurposeEncoder from "../encoding/GeneralPurposeEncoder"; +import {SUPPORTS_SAB_VIEW_DECODE} from "../utils/featureDetection"; import {TransferableState} from "../TransferableState"; import TransferableDataStructure from "../TransferableDataStructure"; @@ -884,14 +885,22 @@ export class ShareableArray extends TransferableDataStructure { // Find the correct value encoder and decode the value at the requested position in the data array const encoder = this.getEncoderById(valueEncoderId); - // Copy from shared memory to a temporary private buffer (since we cannot directly decode from shared memory) const sourceView = new Uint8Array(this.dataView.buffer, dataPos + ShareableArray.DATA_OBJECT_OFFSET, valueLength); - const targetView = new Uint8Array(this.getFittingDecoderBuffer(valueLength), 0, valueLength); - targetView.set(sourceView); + // Optimization: For NumberEncoder (ID 0), we can always read directly from shared memory (via DataView). + // For others (String/General), we check if TextDecoder supports SAB views. + // We avoid direct reads for custom serializers (ID 3) as we can't guarantee they handle SAB views correctly. + const canReadDirectly = valueEncoderId === 0 || ((valueEncoderId === 1 || valueEncoderId === 2) && SUPPORTS_SAB_VIEW_DECODE); + if (canReadDirectly) { + return encoder.decode(sourceView); + } else { + // Copy from shared memory to a temporary private buffer (since we cannot directly decode from shared memory) + const targetView = new Uint8Array(this.getFittingDecoderBuffer(valueLength), 0, valueLength); + targetView.set(sourceView); - return encoder.decode(targetView); + return encoder.decode(targetView); + } } private deleteItem(index: number): void { diff --git a/src/encoding/NumberEncoder.ts b/src/encoding/NumberEncoder.ts index 51e2228..a0a2526 100644 --- a/src/encoding/NumberEncoder.ts +++ b/src/encoding/NumberEncoder.ts @@ -2,7 +2,8 @@ import Serializable from "./Serializable"; export default class NumberEncoder implements Serializable { decode(buffer: Uint8Array): number { - const bufferView = new DataView(buffer.buffer); + // Create DataView respecting the byteOffset of the input buffer (which might be a view into a larger buffer) + const bufferView = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength); // First byte indicates if we did store a float or an int const numberType = bufferView.getUint8(0); diff --git a/src/map/ShareableMap.ts b/src/map/ShareableMap.ts index d3ca305..af2e37b 100644 --- a/src/map/ShareableMap.ts +++ b/src/map/ShareableMap.ts @@ -3,6 +3,7 @@ import Serializable from "./../encoding/Serializable"; import StringEncoder from "./../encoding/StringEncoder"; import NumberEncoder from "./../encoding/NumberEncoder"; import GeneralPurposeEncoder from "./../encoding/GeneralPurposeEncoder"; +import {SUPPORTS_SAB_VIEW_DECODE} from "../utils/featureDetection"; import ShareableMapOptions from "./ShareableMapOptions"; import {TransferableState} from "../TransferableState"; import TransferableDataStructure from "../TransferableDataStructure"; @@ -730,6 +731,10 @@ export class ShareableMap extends TransferableDataStructure { const sourceView = new Uint8Array(this.dataView.buffer, startPos + ShareableMap.DATA_OBJECT_OFFSET, keyLength); + if (SUPPORTS_SAB_VIEW_DECODE) { + return this.textDecoder.decode(sourceView); + } + const targetView = new Uint8Array(this.getFittingDecoderBuffer(keyLength), 0, keyLength); targetView.set(sourceView); @@ -757,11 +762,20 @@ export class ShareableMap extends TransferableDataStructure { const keyLength = this.dataView.getUint32(startPos + 4); const valueLength = this.dataView.getUint32(startPos + 8); - const encoder = this.getEncoderById(this.dataView.getUint16(startPos + 14)); + const valueEncoderId = this.dataView.getUint16(startPos + 14); + const encoder = this.getEncoderById(valueEncoderId); - // Copy from shared memory to a temporary private buffer (since we cannot directly decode from shared memory) const sourceView = new Uint8Array(this.dataView.buffer, startPos + ShareableMap.DATA_OBJECT_OFFSET + keyLength, valueLength); + // Optimization: For NumberEncoder (ID 0), we can always read directly from shared memory. + // For others, we check if TextDecoder supports SAB views. + const canReadDirectly = valueEncoderId === 0 || ((valueEncoderId === 1 || valueEncoderId === 2) && SUPPORTS_SAB_VIEW_DECODE); + + if (canReadDirectly) { + return encoder.decode(sourceView); + } + + // Copy from shared memory to a temporary private buffer (since we cannot directly decode from shared memory) const targetView = new Uint8Array(this.getFittingDecoderBuffer(valueLength), 0, valueLength); targetView.set(sourceView); diff --git a/src/utils/featureDetection.ts b/src/utils/featureDetection.ts new file mode 100644 index 0000000..cf85aaa --- /dev/null +++ b/src/utils/featureDetection.ts @@ -0,0 +1,13 @@ + +export const SUPPORTS_SAB_VIEW_DECODE = (() => { + try { + if (typeof SharedArrayBuffer === 'undefined') return false; + // Create a small SharedArrayBuffer and try to decode a view of it + const sab = new SharedArrayBuffer(1); + const view = new Uint8Array(sab); + new TextDecoder().decode(view); + return true; + } catch (e) { + return false; + } +})();