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
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2025-05-22 - TypedArray loop vs set() with subarray
**Learning:** In V8 (Node 20+), creating `subarray` views inside a hot loop (100k+ iterations) for small blocks (20-30 bytes) adds significant overhead, making `Uint8Array.prototype.set(source.subarray(...))` slower than a simple `for` loop copying bytes between TypedArrays.
**Action:** When copying many small non-contiguous blocks, prefer a manual loop over TypedArrays rather than creating many temporary views. Use `set()` only for large contiguous blocks.
9 changes: 5 additions & 4 deletions src/array/ShareableArray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -945,7 +945,8 @@ export class ShareableArray<T> extends TransferableDataStructure {
*/
private defragment() {
const newData: ArrayBuffer = new ArrayBuffer(this.dataView.byteLength);
const newView = new DataView(newData);
const newBytes = new Uint8Array(newData);
const oldBytes = new Uint8Array(this.dataMem);

let currentDataStart = 0;

Expand All @@ -956,15 +957,15 @@ export class ShareableArray<T> extends TransferableDataStructure {
const currentObjectLength = this.dataView.getUint32(currentDataPos + 4) + ShareableArray.DATA_OBJECT_OFFSET;

// Copy all bytes to the new array
for (let i = 0; i < currentObjectLength; i++) {
newView.setUint8(currentDataStart + i, this.dataView.getUint8(currentDataPos + i));
for (let k = 0; k < currentObjectLength; k++) {
newBytes[currentDataStart + k] = oldBytes[currentDataPos + k];
}

// Update the position where this is stored in the index array
this.indexView.setUint32(ShareableArray.INDEX_TABLE_OFFSET + 4 * i, currentDataStart);

// Update the starting position in the new defragmented array
currentDataStart += currentObjectLength + ShareableArray.DATA_OBJECT_OFFSET;
currentDataStart += currentObjectLength;
}

// Replace the data from the old data array with the data in the array
Expand Down
32 changes: 30 additions & 2 deletions src/array/__tests__/ShareableArray.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -629,8 +629,36 @@ describe("ShareableArray", () => {
expect(sorted).toBeInstanceOf(ShareableArray);
expect(customSorted).toBeInstanceOf(ShareableArray);
});



it("should correctly defragment the array", () => {
const array = new ShareableArray<string>();
const items = ["item1", "item2", "item3", "item4", "item5"];
items.forEach(item => array.push(item));

// Delete some items to create gaps
array.pop(); // delete last
(array as any).deleteItem(1); // delete index 1 ("item2")

// Array should have [item1, item3, item4]
// Indices: 0->item1, 1->item3, 2->item4

expect(array.length).toBe(3);
expect(array.at(0)).toBe("item1");
expect(array.at(1)).toBe("item3");
expect(array.at(2)).toBe("item4");

// Call defragment
(array as any).defragment();

// Verify data is still intact
expect(array.length).toBe(3);
expect(array.at(0)).toBe("item1");
expect(array.at(1)).toBe("item3");
expect(array.at(2)).toBe("item4");

// We can't easily verify memory layout without inspecting private internals,
// but if data retrieval works, it means defrag didn't corrupt pointers.
});
});

function generateRandomString() {
Expand Down