mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
## Summary
Add a fast path for `structuredClone` and `postMessage` when the root
value is a dense array of primitives or strings. This bypasses the full
`CloneSerializer`/`CloneDeserializer` machinery by keeping data in
native C++ structures instead of serializing to a byte stream.
**Important:** This optimization only applies when the root value passed
to `structuredClone()` / `postMessage()` is an array. Nested arrays
within objects still go through the normal serialization path.
## Implementation
Three tiers of array fast paths, checked in order:
| Tier | Indexing Type | Strategy | Applies When |
|------|--------------|----------|--------------|
| **Tier 1** | `ArrayWithInt32` | `memcpy` butterfly data | Dense int32
array, no holes, no named properties |
| **Tier 2** | `ArrayWithDouble` | `memcpy` butterfly data | Dense
double array, no holes, no named properties |
| **Tier 3** | `ArrayWithContiguous` | Copy elements into
`FixedVector<variant<JSValue, String>>` | Dense array of
primitives/strings, no holes, no named properties |
All tiers fall through to the normal serialization path when:
- The array has holes that must forward to the prototype
- The array has named properties (e.g., `arr.foo = "bar"`) — checked via
`structure->maxOffset() != invalidOffset`
- Elements contain non-primitive, non-string values (objects, arrays,
etc.)
- The context requires wire-format serialization (storage, cross-process
transfer)
### Deserialization
- **Tier 1/2:** Allocate a new `Butterfly` via `vm.auxiliarySpace()`,
`memcpy` data back, create array with `JSArray::createWithButterfly()`.
Falls back to normal deserialization if `isHavingABadTime` (forced
ArrayStorage mode).
- **Tier 3:** Pre-convert elements to `JSValue` (including `jsString()`
allocation), then use `JSArray::tryCreateUninitializedRestricted()` +
`initializeIndex()`.
## Benchmarks
Apple M4 Max, comparing system Bun 1.3.8 vs this branch (release build):
| Benchmark | Before | After | Speedup |
|-----------|--------|-------|---------|
| `structuredClone([10 numbers])` | 308.71 ns | 40.38 ns | **7.6x** |
| `structuredClone([100 numbers])` | 1.62 µs | 86.87 ns | **18.7x** |
| `structuredClone([1000 numbers])` | 13.79 µs | 544.56 ns | **25.3x** |
| `structuredClone([10 strings])` | 642.38 ns | 307.38 ns | **2.1x** |
| `structuredClone([100 strings])` | 5.67 µs | 2.57 µs | **2.2x** |
| `structuredClone([10 mixed])` | 446.32 ns | 198.35 ns | **2.3x** |
| `structuredClone(nested array)` | 1.84 µs | 1.79 µs | 1.0x (not
eligible) |
| `structuredClone({a: 123})` | 95.98 ns | 100.07 ns | 1.0x (no
regression) |
Int32 arrays see the largest gains (up to 25x) since they use a direct
`memcpy` of butterfly memory. String/mixed arrays see ~2x improvement.
No performance regression on non-eligible inputs.
## Bug Fix
Also fixes a correctness bug where arrays with named properties (e.g.,
`arr.foo = "bar"`) would lose those properties when going through the
array fast path. Added a `structure->maxOffset() != invalidOffset` guard
to fall back to normal serialization for such arrays.
Fixed a minor double-counting issue in `computeMemoryCost` where
`JSValue` elements in `SimpleArray` were counted both by `byteSize()`
and individually.
## Test Plan
38 tests in `test/js/web/structured-clone-fastpath.test.ts` covering:
- Basic array types: empty, numbers, strings, mixed primitives, special
numbers (`-0`, `NaN`, `Infinity`)
- Large arrays (10,000 elements)
- Tier 2: double arrays, Int32→Double transition
- Deep clone independence verification
- Named properties on Int32, Double, and Contiguous arrays
- `postMessage` via `MessageChannel` for Int32, Double, and mixed arrays
- Edge cases: frozen/sealed arrays, deleted elements (holes), `length`
extension, single-element arrays
- Prototype modification (custom prototype, indexed prototype properties
with holes)
- `Array` subclass identity loss (per spec)
- `undefined`-only and `null`-only arrays
- Multiple independent clones from the same source
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
375 lines
12 KiB
TypeScript
375 lines
12 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
|
|
describe("Structured Clone Fast Path", () => {
|
|
test("structuredClone should work with empty object", () => {
|
|
const object = {};
|
|
const cloned = structuredClone(object);
|
|
expect(cloned).toStrictEqual({});
|
|
});
|
|
|
|
test("structuredClone should work with empty string", () => {
|
|
const string = "";
|
|
const cloned = structuredClone(string);
|
|
expect(cloned).toStrictEqual("");
|
|
});
|
|
|
|
const deOptimizations = [
|
|
{
|
|
get accessor() {
|
|
return 1;
|
|
},
|
|
},
|
|
Object.create(Object.prototype, {
|
|
data: {
|
|
value: 1,
|
|
writable: false,
|
|
configurable: false,
|
|
},
|
|
}),
|
|
Object.create(Object.prototype, {
|
|
data: {
|
|
value: 1,
|
|
writable: true,
|
|
configurable: false,
|
|
},
|
|
}),
|
|
Object.create(Object.prototype, {
|
|
data: {
|
|
get: () => 1,
|
|
configurable: true,
|
|
},
|
|
}),
|
|
Object.create(Object.prototype, {
|
|
data: {
|
|
set: () => {},
|
|
enumerable: true,
|
|
configurable: true,
|
|
},
|
|
}),
|
|
];
|
|
|
|
for (const deOptimization of deOptimizations) {
|
|
test("structuredCloneDeOptimization", () => {
|
|
structuredClone(deOptimization);
|
|
});
|
|
}
|
|
|
|
test("structuredClone should use a constant amount of memory for string inputs", () => {
|
|
const clones: Array<string> = [];
|
|
// Create a 512KB string to test fast path
|
|
const largeString = Buffer.alloc(512 * 1024, "a").toString();
|
|
for (let i = 0; i < 100; i++) {
|
|
clones.push(structuredClone(largeString));
|
|
}
|
|
Bun.gc(true);
|
|
const rss = process.memoryUsage.rss();
|
|
for (let i = 0; i < 10000; i++) {
|
|
clones.push(structuredClone(largeString));
|
|
}
|
|
Bun.gc(true);
|
|
const rss2 = process.memoryUsage.rss();
|
|
const delta = rss2 - rss;
|
|
expect(delta).toBeLessThan(1024 * 1024 * 8);
|
|
expect(clones.length).toBe(10000 + 100);
|
|
});
|
|
|
|
test("structuredClone should use a constant amount of memory for simple object inputs", () => {
|
|
// Create a 512KB string to test fast path
|
|
const largeValue = { property: Buffer.alloc(512 * 1024, "a").toString() };
|
|
for (let i = 0; i < 100; i++) {
|
|
structuredClone(largeValue);
|
|
}
|
|
Bun.gc(true);
|
|
const rss = process.memoryUsage.rss();
|
|
for (let i = 0; i < 10000; i++) {
|
|
structuredClone(largeValue);
|
|
}
|
|
Bun.gc(true);
|
|
const rss2 = process.memoryUsage.rss();
|
|
const delta = rss2 - rss;
|
|
expect(delta).toBeLessThan(1024 * 1024);
|
|
});
|
|
|
|
// === Array fast path tests ===
|
|
|
|
test("structuredClone should work with empty array", () => {
|
|
expect(structuredClone([])).toEqual([]);
|
|
});
|
|
|
|
test("structuredClone should work with array of numbers", () => {
|
|
const input = [1, 2, 3, 4, 5];
|
|
expect(structuredClone(input)).toEqual(input);
|
|
});
|
|
|
|
test("structuredClone should work with array of strings", () => {
|
|
const input = ["hello", "world", ""];
|
|
expect(structuredClone(input)).toEqual(input);
|
|
});
|
|
|
|
test("structuredClone should work with array of mixed primitives", () => {
|
|
const input = [1, "hello", true, false, null, undefined, 3.14];
|
|
const cloned = structuredClone(input);
|
|
expect(cloned).toEqual(input);
|
|
});
|
|
|
|
test("structuredClone should work with array of special numbers", () => {
|
|
const cloned = structuredClone([-0, NaN, Infinity, -Infinity]);
|
|
expect(Object.is(cloned[0], -0)).toBe(true);
|
|
expect(cloned[1]).toBeNaN();
|
|
expect(cloned[2]).toBe(Infinity);
|
|
expect(cloned[3]).toBe(-Infinity);
|
|
});
|
|
|
|
test("structuredClone should work with large array of numbers", () => {
|
|
const input = Array.from({ length: 10000 }, (_, i) => i);
|
|
expect(structuredClone(input)).toEqual(input);
|
|
});
|
|
|
|
test("structuredClone should fallback for arrays with nested objects", () => {
|
|
const input = [{ a: 1 }, { b: 2 }];
|
|
expect(structuredClone(input)).toEqual(input);
|
|
});
|
|
|
|
test("structuredClone should fallback for arrays with holes", () => {
|
|
const input = [1, , 3]; // sparse
|
|
const cloned = structuredClone(input);
|
|
// structured clone spec: holes become undefined
|
|
expect(cloned[0]).toBe(1);
|
|
expect(cloned[1]).toBe(undefined);
|
|
expect(cloned[2]).toBe(3);
|
|
});
|
|
|
|
test("structuredClone should work with array of doubles", () => {
|
|
const input = [1.5, 2.7, 3.14, 0.1 + 0.2];
|
|
const cloned = structuredClone(input);
|
|
expect(cloned).toEqual(input);
|
|
});
|
|
|
|
test("structuredClone creates independent copy of array", () => {
|
|
const input = [1, 2, 3];
|
|
const cloned = structuredClone(input);
|
|
cloned[0] = 999;
|
|
expect(input[0]).toBe(1);
|
|
});
|
|
|
|
test("structuredClone should preserve named properties on arrays", () => {
|
|
const input: any = [1, 2, 3];
|
|
input.foo = "bar";
|
|
const cloned = structuredClone(input);
|
|
expect(cloned.foo).toBe("bar");
|
|
expect(Array.from(cloned)).toEqual([1, 2, 3]);
|
|
});
|
|
|
|
test("postMessage should work with array fast path", async () => {
|
|
const { port1, port2 } = new MessageChannel();
|
|
const input = [1, 2, 3, "hello", true];
|
|
const { promise, resolve } = Promise.withResolvers();
|
|
port2.onmessage = (e: MessageEvent) => resolve(e.data);
|
|
port1.postMessage(input);
|
|
const result = await promise;
|
|
expect(result).toEqual(input);
|
|
port1.close();
|
|
port2.close();
|
|
});
|
|
|
|
// === Edge case tests ===
|
|
|
|
test("structuredClone of frozen array should produce a non-frozen clone", () => {
|
|
const input = Object.freeze([1, 2, 3]);
|
|
const cloned = structuredClone(input);
|
|
expect(cloned).toEqual([1, 2, 3]);
|
|
expect(Object.isFrozen(cloned)).toBe(false);
|
|
cloned[0] = 999;
|
|
expect(cloned[0]).toBe(999);
|
|
});
|
|
|
|
test("structuredClone of sealed array should produce a non-sealed clone", () => {
|
|
const input = Object.seal([1, 2, 3]);
|
|
const cloned = structuredClone(input);
|
|
expect(cloned).toEqual([1, 2, 3]);
|
|
expect(Object.isSealed(cloned)).toBe(false);
|
|
cloned.push(4);
|
|
expect(cloned).toEqual([1, 2, 3, 4]);
|
|
});
|
|
|
|
test("structuredClone of array with deleted element (hole via delete)", () => {
|
|
const input = [1, 2, 3];
|
|
delete (input as any)[1];
|
|
const cloned = structuredClone(input);
|
|
expect(cloned[0]).toBe(1);
|
|
expect(cloned[1]).toBe(undefined);
|
|
expect(cloned[2]).toBe(3);
|
|
expect(1 in cloned).toBe(false); // holes remain holes after structuredClone
|
|
});
|
|
|
|
test("structuredClone of array with length > actual elements", () => {
|
|
const input = [1, 2, 3];
|
|
input.length = 6;
|
|
const cloned = structuredClone(input);
|
|
expect(cloned.length).toBe(6);
|
|
expect(cloned[0]).toBe(1);
|
|
expect(cloned[1]).toBe(2);
|
|
expect(cloned[2]).toBe(3);
|
|
expect(cloned[3]).toBe(undefined);
|
|
});
|
|
|
|
test("structuredClone of single element arrays", () => {
|
|
expect(structuredClone([42])).toEqual([42]);
|
|
expect(structuredClone([3.14])).toEqual([3.14]);
|
|
expect(structuredClone(["hello"])).toEqual(["hello"]);
|
|
expect(structuredClone([true])).toEqual([true]);
|
|
expect(structuredClone([null])).toEqual([null]);
|
|
});
|
|
|
|
test("structuredClone of array with named properties on Int32 array", () => {
|
|
const input: any = [1, 2, 3]; // Int32 indexing
|
|
input.name = "test";
|
|
input.count = 42;
|
|
const cloned = structuredClone(input);
|
|
expect(cloned.name).toBe("test");
|
|
expect(cloned.count).toBe(42);
|
|
expect(Array.from(cloned)).toEqual([1, 2, 3]);
|
|
});
|
|
|
|
test("structuredClone of array with named properties on Double array", () => {
|
|
const input: any = [1.1, 2.2, 3.3]; // Double indexing
|
|
input.label = "doubles";
|
|
const cloned = structuredClone(input);
|
|
expect(cloned.label).toBe("doubles");
|
|
expect(Array.from(cloned)).toEqual([1.1, 2.2, 3.3]);
|
|
});
|
|
|
|
test("structuredClone of array that transitions Int32 to Double", () => {
|
|
const input = [1, 2, 3]; // starts as Int32
|
|
input.push(4.5); // transitions to Double
|
|
const cloned = structuredClone(input);
|
|
expect(cloned).toEqual([1, 2, 3, 4.5]);
|
|
});
|
|
|
|
test("structuredClone of array with modified prototype", () => {
|
|
const input = [1, 2, 3];
|
|
Object.setPrototypeOf(input, {
|
|
customMethod() {
|
|
return 42;
|
|
},
|
|
});
|
|
const cloned = structuredClone(input);
|
|
// Clone should have standard Array prototype, not the custom one
|
|
expect(Array.from(cloned)).toEqual([1, 2, 3]);
|
|
expect(cloned).toBeInstanceOf(Array);
|
|
expect((cloned as any).customMethod).toBeUndefined();
|
|
});
|
|
|
|
test("structuredClone of array with prototype indexed properties and holes", () => {
|
|
const proto = Object.create(Array.prototype);
|
|
proto[1] = "from proto";
|
|
const input = new Array(3);
|
|
Object.setPrototypeOf(input, proto);
|
|
input[0] = "a";
|
|
input[2] = "c";
|
|
// structuredClone only copies own properties; prototype values are not included
|
|
const cloned = structuredClone(input);
|
|
expect(cloned[0]).toBe("a");
|
|
expect(1 in cloned).toBe(false); // hole, not "from proto"
|
|
expect(cloned[2]).toBe("c");
|
|
expect(cloned).toBeInstanceOf(Array);
|
|
});
|
|
|
|
test("postMessage with Int32 array via MessageChannel", async () => {
|
|
const { port1, port2 } = new MessageChannel();
|
|
const input = [10, 20, 30, 40, 50];
|
|
const { promise, resolve } = Promise.withResolvers();
|
|
port2.onmessage = (e: MessageEvent) => resolve(e.data);
|
|
port1.postMessage(input);
|
|
const result = await promise;
|
|
expect(result).toEqual(input);
|
|
port1.close();
|
|
port2.close();
|
|
});
|
|
|
|
test("postMessage with Double array via MessageChannel", async () => {
|
|
const { port1, port2 } = new MessageChannel();
|
|
const input = [1.1, 2.2, 3.3];
|
|
const { promise, resolve } = Promise.withResolvers();
|
|
port2.onmessage = (e: MessageEvent) => resolve(e.data);
|
|
port1.postMessage(input);
|
|
const result = await promise;
|
|
expect(result).toEqual(input);
|
|
port1.close();
|
|
port2.close();
|
|
});
|
|
|
|
test("structuredClone of array multiple times produces independent copies", () => {
|
|
const input = [1, 2, 3];
|
|
const clones = Array.from({ length: 10 }, () => structuredClone(input));
|
|
clones[0][0] = 999;
|
|
clones[5][1] = 888;
|
|
// All other clones and the original should be unaffected
|
|
expect(input).toEqual([1, 2, 3]);
|
|
for (let i = 1; i < 10; i++) {
|
|
if (i === 5) {
|
|
expect(clones[i]).toEqual([1, 888, 3]);
|
|
} else {
|
|
expect(clones[i]).toEqual([1, 2, 3]);
|
|
}
|
|
}
|
|
});
|
|
|
|
test("structuredClone of Array subclass loses subclass identity", () => {
|
|
class MyArray extends Array {
|
|
customProp = "hello";
|
|
sum() {
|
|
return this.reduce((a: number, b: number) => a + b, 0);
|
|
}
|
|
}
|
|
const input = new MyArray(1, 2, 3);
|
|
input.customProp = "world";
|
|
const cloned = structuredClone(input);
|
|
// structuredClone spec: result is a plain Array, not a subclass
|
|
expect(Array.from(cloned)).toEqual([1, 2, 3]);
|
|
expect(cloned).toBeInstanceOf(Array);
|
|
expect((cloned as any).sum).toBeUndefined();
|
|
});
|
|
|
|
test("structuredClone of array with only undefined values", () => {
|
|
const input = [undefined, undefined, undefined];
|
|
const cloned = structuredClone(input);
|
|
expect(cloned).toEqual([undefined, undefined, undefined]);
|
|
expect(cloned.length).toBe(3);
|
|
// Ensure they are actual values, not holes
|
|
expect(0 in cloned).toBe(true);
|
|
expect(1 in cloned).toBe(true);
|
|
expect(2 in cloned).toBe(true);
|
|
});
|
|
|
|
test("structuredClone of array with only null values", () => {
|
|
const input = [null, null, null];
|
|
const cloned = structuredClone(input);
|
|
expect(cloned).toEqual([null, null, null]);
|
|
});
|
|
|
|
test("structuredClone of dense double array preserves -0 and NaN", () => {
|
|
const input = [-0, NaN, -0, NaN];
|
|
const cloned = structuredClone(input);
|
|
expect(Object.is(cloned[0], -0)).toBe(true);
|
|
expect(cloned[1]).toBeNaN();
|
|
expect(Object.is(cloned[2], -0)).toBe(true);
|
|
expect(cloned[3]).toBeNaN();
|
|
});
|
|
|
|
test("structuredClone on object with simple properties can exceed JSFinalObject::maxInlineCapacity", () => {
|
|
let largeValue = {};
|
|
for (let i = 0; i < 100; i++) {
|
|
largeValue["property" + i] = i;
|
|
}
|
|
|
|
for (let i = 0; i < 100; i++) {
|
|
expect(structuredClone(largeValue)).toStrictEqual(largeValue);
|
|
}
|
|
Bun.gc(true);
|
|
for (let i = 0; i < 100; i++) {
|
|
expect(structuredClone(largeValue)).toStrictEqual(largeValue);
|
|
}
|
|
});
|
|
});
|