Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -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.
17 changes: 13 additions & 4 deletions src/array/ShareableArray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -884,14 +885,22 @@ export class ShareableArray<T> 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 {
Expand Down
3 changes: 2 additions & 1 deletion src/encoding/NumberEncoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import Serializable from "./Serializable";

export default class NumberEncoder implements Serializable<number> {
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);
Expand Down
18 changes: 16 additions & 2 deletions src/map/ShareableMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -730,6 +731,10 @@ export class ShareableMap<K, V> 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);

Expand Down Expand Up @@ -757,11 +762,20 @@ export class ShareableMap<K, V> 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);

Expand Down
13 changes: 13 additions & 0 deletions src/utils/featureDetection.ts
Original file line number Diff line number Diff line change
@@ -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;
}
})();