Compare commits

...

5 Commits

Author SHA1 Message Date
Sosuke Suzuki
1e125b725d fix(clone): move coversFullBuffer check after isDetached to prevent null deref
The coversFullBuffer check called possiblySharedBuffer()->byteLength()
before verifying !isDetached(). On a detached WastefulTypedArray,
possiblySharedBuffer() returns null, causing a segfault.

Move the hasArrayBuffer/possiblySharedBuffer check into the if-chain
after isDetached() so short-circuit evaluation prevents the null deref.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 01:35:58 +09:00
Sosuke Suzuki
72e1c6577c fix(clone): exclude partial-buffer TypedArray views from fast path
new Uint8Array(buf, 0, 8) over a 16-byte buffer has byteOffset==0 but
only covers half the buffer. The fast path copied only the view's bytes,
producing cloned.buffer.byteLength==8 instead of the spec-correct 16.

Add a coversFullBuffer check: for WastefulTypedArray (hasArrayBuffer),
compare view->byteLength() against the backing buffer's byteLength.
FastTypedArray/OversizeTypedArray always cover their data entirely.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 14:43:36 +09:00
Sosuke Suzuki
3cb5c4a722 fix(clone): exclude SharedArrayBuffer-backed TypedArrays from fast path
isResizableOrGrowableShared() only returns true for resizable/growable
shared views — a regular SharedArrayBuffer (new SharedArrayBuffer(n))
passes through. Add view->isShared() check to prevent the fast path
from copying SAB data into an unshared buffer, which would break
shared-memory semantics.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 14:43:36 +09:00
autofix-ci[bot]
30ca7c952b [autofix.ci] apply automated fixes 2026-02-13 02:59:22 +00:00
Sosuke Suzuki
c153188363 perf(clone): add structuredClone/postMessage fast path for TypedArray
Skip the CloneSerializer/CloneDeserializer byte-buffer pipeline for
TypedArray values (Uint8Array, Float64Array, etc.) by storing the raw
bytes in-memory and reconstructing the typed view directly on
deserialization. This avoids intermediate serialization overhead and
yields ~2-2.8x speedup for small/medium TypedArrays (64B-4KB).

Fast path conditions: not detached, not out-of-bounds, not
resizable/growable, byteOffset == 0, no named properties.
All other cases fall through to the existing slow path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 11:22:50 +09:00
5 changed files with 478 additions and 1 deletions

View File

@@ -0,0 +1,68 @@
import { bench, group, run, summary } from "../runner.mjs";
// === TypedArray structuredClone benchmarks ===
// Uint8Array at various sizes
var uint8_64 = new Uint8Array(64);
var uint8_1K = new Uint8Array(1024);
var uint8_64K = new Uint8Array(64 * 1024);
var uint8_1M = new Uint8Array(1024 * 1024);
// Fill with non-zero data to be realistic
for (var i = 0; i < uint8_64.length; i++) uint8_64[i] = i & 0xff;
for (var i = 0; i < uint8_1K.length; i++) uint8_1K[i] = i & 0xff;
for (var i = 0; i < uint8_64K.length; i++) uint8_64K[i] = i & 0xff;
for (var i = 0; i < uint8_1M.length; i++) uint8_1M[i] = i & 0xff;
// Other typed array types (1KB each)
var int8_1K = new Int8Array(1024);
var uint16_1K = new Uint16Array(512); // 1KB
var int32_1K = new Int32Array(256); // 1KB
var float32_1K = new Float32Array(256); // 1KB
var float64_1K = new Float64Array(128); // 1KB
var bigint64_1K = new BigInt64Array(128); // 1KB
for (var i = 0; i < int8_1K.length; i++) int8_1K[i] = (i % 256) - 128;
for (var i = 0; i < uint16_1K.length; i++) uint16_1K[i] = i;
for (var i = 0; i < int32_1K.length; i++) int32_1K[i] = i * 1000;
for (var i = 0; i < float32_1K.length; i++) float32_1K[i] = i * 0.1;
for (var i = 0; i < float64_1K.length; i++) float64_1K[i] = i * 0.1;
for (var i = 0; i < bigint64_1K.length; i++) bigint64_1K[i] = BigInt(i);
// Slice view (byteOffset != 0) — should fall back to slow path
var sliceBuf = new ArrayBuffer(2048);
var uint8_slice = new Uint8Array(sliceBuf, 512, 512);
summary(() => {
group("Uint8Array by size", () => {
bench("Uint8Array 64B", () => structuredClone(uint8_64));
bench("Uint8Array 1KB", () => structuredClone(uint8_1K));
bench("Uint8Array 64KB", () => structuredClone(uint8_64K));
bench("Uint8Array 1MB", () => structuredClone(uint8_1M));
});
});
summary(() => {
group("TypedArray types (1KB each)", () => {
bench("Int8Array", () => structuredClone(int8_1K));
bench("Uint8Array", () => structuredClone(uint8_1K));
bench("Uint16Array", () => structuredClone(uint16_1K));
bench("Int32Array", () => structuredClone(int32_1K));
bench("Float32Array", () => structuredClone(float32_1K));
bench("Float64Array", () => structuredClone(float64_1K));
bench("BigInt64Array", () => structuredClone(bigint64_1K));
});
});
// Pre-create for fair comparison
var uint8_whole = new Uint8Array(512);
for (var i = 0; i < 512; i++) uint8_whole[i] = i & 0xff;
summary(() => {
group("fast path vs slow path (512B)", () => {
bench("Uint8Array whole (fast path)", () => structuredClone(uint8_whole));
bench("Uint8Array slice (slow path)", () => structuredClone(uint8_slice));
});
});
await run();

View File

@@ -59,4 +59,15 @@ var objectsMedium = Array.from({ length: 100 }, (_, i) => ({ id: i, name: `item-
bench("structuredClone([10 objects])", () => structuredClone(objectsSmall));
bench("structuredClone([100 objects])", () => structuredClone(objectsMedium));
// TypedArray fast path targets
var uint8Small = new Uint8Array(64);
var uint8Medium = new Uint8Array(1024);
var uint8Large = new Uint8Array(1024 * 1024);
var float64Medium = new Float64Array(128);
bench("structuredClone(Uint8Array 64B)", () => structuredClone(uint8Small));
bench("structuredClone(Uint8Array 1KB)", () => structuredClone(uint8Medium));
bench("structuredClone(Uint8Array 1MB)", () => structuredClone(uint8Large));
bench("structuredClone(Float64Array 1KB)", () => structuredClone(float64Medium));
await run();

View File

@@ -95,6 +95,7 @@
#include <JavaScriptCore/RegExp.h>
#include <JavaScriptCore/RegExpObject.h>
#include <JavaScriptCore/TypedArrayInlines.h>
#include <JavaScriptCore/TypedArrayType.h>
#include <JavaScriptCore/TypedArrays.h>
#include <JavaScriptCore/WasmModule.h>
#include <JavaScriptCore/YarrFlags.h>
@@ -385,6 +386,38 @@ static unsigned typedArrayElementSize(ArrayBufferViewSubtag tag)
}
}
static ArrayBufferViewSubtag subtagForTypedArrayType(TypedArrayType type)
{
switch (type) {
case TypeInt8:
return Int8ArrayTag;
case TypeUint8:
return Uint8ArrayTag;
case TypeUint8Clamped:
return Uint8ClampedArrayTag;
case TypeInt16:
return Int16ArrayTag;
case TypeUint16:
return Uint16ArrayTag;
case TypeInt32:
return Int32ArrayTag;
case TypeUint32:
return Uint32ArrayTag;
case TypeFloat16:
return Float16ArrayTag;
case TypeFloat32:
return Float32ArrayTag;
case TypeFloat64:
return Float64ArrayTag;
case TypeBigInt64:
return BigInt64ArrayTag;
case TypeBigUint64:
return BigUint64ArrayTag;
default:
return DataViewTag;
}
}
enum class SerializableErrorType : uint8_t {
Error,
EvalError,
@@ -5606,6 +5639,14 @@ SerializedScriptValue::SerializedScriptValue(WTF::FixedVector<DenseArrayElement>
m_memoryCost = computeMemoryCost();
}
SerializedScriptValue::SerializedScriptValue(Vector<uint8_t>&& data, uint8_t subtag)
: m_arrayButterflyData(WTF::move(data))
, m_fastPath(FastPath::TypedArray)
, m_typedArraySubtag(subtag)
{
m_memoryCost = computeMemoryCost();
}
Ref<SerializedScriptValue> SerializedScriptValue::createDenseArrayFastPath(
WTF::FixedVector<DenseArrayElement>&& elements)
{
@@ -5695,6 +5736,7 @@ size_t SerializedScriptValue::computeMemoryCost() const
break;
case FastPath::Int32Array:
case FastPath::DoubleArray:
case FastPath::TypedArray:
cost += m_arrayButterflyData.size();
break;
case FastPath::DenseArray:
@@ -5916,7 +5958,35 @@ ExceptionOr<Ref<SerializedScriptValue>> SerializedScriptValue::create(JSGlobalOb
object = cell->getObject();
structure = object->structure();
if (auto* jsArray = jsDynamicCast<JSArray*>(object)) {
// TypedArray fast path: check before JSArray since TypedArray is not a JSArray
auto jsType = structure->typeInfo().type();
if (isTypedView(jsType)) {
auto* view = jsCast<JSArrayBufferView*>(object);
size_t byteLength = view->byteLength();
if (!view->isDetached()
&& !view->isOutOfBounds()
&& !view->isShared()
&& !view->isResizableOrGrowableShared()
&& view->byteOffset() == 0
&& structure->maxOffset() == invalidOffset
// For WastefulTypedArray (hasArrayBuffer()==true), verify the view
// covers the full ArrayBuffer; partial views (e.g. new Uint8Array(buf, 0, 8)
// over a 16-byte buffer) must fall through to the slow path.
// possiblySharedBuffer() is safe after isDetached()/isShared() checks:
// WastefulTypedArray just returns existingBufferInButterfly() without
// triggering slowDownAndWasteMemory(). Must be evaluated AFTER isDetached()
// to avoid null deref on detached buffers.
&& (!view->hasArrayBuffer()
|| view->byteLength() == view->possiblySharedBuffer()->byteLength())) {
auto taType = typedArrayType(jsType);
auto subtag = subtagForTypedArrayType(taType);
auto* data = static_cast<const uint8_t*>(view->vector());
// Use span constructor: single allocation + memcpy, no zero-fill
Vector<uint8_t> buffer(std::span<const uint8_t> { data, byteLength });
return SerializedScriptValue::createTypedArrayFastPath(WTF::move(buffer), static_cast<uint8_t>(subtag));
}
// Conditions not met → fall through to slow path
} else if (auto* jsArray = jsDynamicCast<JSArray*>(object)) {
canUseArrayFastPath = true;
array = jsArray;
} else if (isObjectFastPathCandidate(structure)) {
@@ -6366,6 +6436,11 @@ Ref<SerializedScriptValue> SerializedScriptValue::createDoubleArrayFastPath(Vect
return adoptRef(*new SerializedScriptValue(WTF::move(data), length, FastPath::DoubleArray));
}
Ref<SerializedScriptValue> SerializedScriptValue::createTypedArrayFastPath(Vector<uint8_t>&& data, uint8_t subtag)
{
return adoptRef(*new SerializedScriptValue(WTF::move(data), subtag));
}
RefPtr<SerializedScriptValue> SerializedScriptValue::create(JSContextRef originContext, JSValueRef apiValue, JSValueRef* exception)
{
JSGlobalObject* lexicalGlobalObject = toJS(originContext);
@@ -6676,6 +6751,71 @@ JSValue SerializedScriptValue::deserialize(JSGlobalObject& lexicalGlobalObject,
*didFail = false;
return resultArray;
}
case FastPath::TypedArray: {
size_t byteLength = m_arrayButterflyData.size();
auto subtag = static_cast<ArrayBufferViewSubtag>(m_typedArraySubtag);
unsigned elemSize = typedArrayElementSize(subtag);
if (!elemSize) [[unlikely]]
break;
auto arrayBuffer = ArrayBuffer::tryCreate(m_arrayButterflyData.span());
if (!arrayBuffer) [[unlikely]] {
if (didFail)
*didFail = true;
return {};
}
std::optional<size_t> length = byteLength / elemSize;
JSValue typedArrayValue;
switch (subtag) {
case Int8ArrayTag:
typedArrayValue = toJS(&lexicalGlobalObject, globalObject, Int8Array::wrappedAs(arrayBuffer.releaseNonNull(), 0, length).get());
break;
case Uint8ArrayTag:
typedArrayValue = toJS(&lexicalGlobalObject, globalObject, Uint8Array::wrappedAs(arrayBuffer.releaseNonNull(), 0, length).get());
break;
case Uint8ClampedArrayTag:
typedArrayValue = toJS(&lexicalGlobalObject, globalObject, Uint8ClampedArray::wrappedAs(arrayBuffer.releaseNonNull(), 0, length).get());
break;
case Int16ArrayTag:
typedArrayValue = toJS(&lexicalGlobalObject, globalObject, Int16Array::wrappedAs(arrayBuffer.releaseNonNull(), 0, length).get());
break;
case Uint16ArrayTag:
typedArrayValue = toJS(&lexicalGlobalObject, globalObject, Uint16Array::wrappedAs(arrayBuffer.releaseNonNull(), 0, length).get());
break;
case Int32ArrayTag:
typedArrayValue = toJS(&lexicalGlobalObject, globalObject, Int32Array::wrappedAs(arrayBuffer.releaseNonNull(), 0, length).get());
break;
case Uint32ArrayTag:
typedArrayValue = toJS(&lexicalGlobalObject, globalObject, Uint32Array::wrappedAs(arrayBuffer.releaseNonNull(), 0, length).get());
break;
case Float16ArrayTag:
typedArrayValue = toJS(&lexicalGlobalObject, globalObject, Float16Array::wrappedAs(arrayBuffer.releaseNonNull(), 0, length).get());
break;
case Float32ArrayTag:
typedArrayValue = toJS(&lexicalGlobalObject, globalObject, Float32Array::wrappedAs(arrayBuffer.releaseNonNull(), 0, length).get());
break;
case Float64ArrayTag:
typedArrayValue = toJS(&lexicalGlobalObject, globalObject, Float64Array::wrappedAs(arrayBuffer.releaseNonNull(), 0, length).get());
break;
case BigInt64ArrayTag:
typedArrayValue = toJS(&lexicalGlobalObject, globalObject, BigInt64Array::wrappedAs(arrayBuffer.releaseNonNull(), 0, length).get());
break;
case BigUint64ArrayTag:
typedArrayValue = toJS(&lexicalGlobalObject, globalObject, BigUint64Array::wrappedAs(arrayBuffer.releaseNonNull(), 0, length).get());
break;
default:
break;
}
if (typedArrayValue) {
if (didFail)
*didFail = false;
return typedArrayValue;
}
break;
}
case FastPath::None: {
break;
}

View File

@@ -87,6 +87,7 @@ enum class FastPath : uint8_t {
Int32Array,
DoubleArray,
DenseArray,
TypedArray,
};
#if ENABLE(OFFSCREEN_CANVAS_IN_WORKERS)
@@ -148,6 +149,9 @@ public:
// Fast path for postMessage with dense arrays containing simple objects
static Ref<SerializedScriptValue> createDenseArrayFastPath(WTF::FixedVector<DenseArrayElement>&& elements);
// Fast path for postMessage with TypedArray (Uint8Array, Float64Array, etc.)
static Ref<SerializedScriptValue> createTypedArrayFastPath(Vector<uint8_t>&& data, uint8_t subtag);
static Ref<SerializedScriptValue> nullValue();
WEBCORE_EXPORT JSC::JSValue deserialize(JSC::JSGlobalObject&, JSC::JSGlobalObject*, SerializationErrorMode = SerializationErrorMode::Throwing, bool* didFail = nullptr);
@@ -255,6 +259,8 @@ private:
SerializedScriptValue(Vector<uint8_t>&& butterflyData, uint32_t length, FastPath fastPath);
// Constructor for DenseArray fast path
explicit SerializedScriptValue(WTF::FixedVector<DenseArrayElement>&& denseElements);
// Constructor for TypedArray fast path
SerializedScriptValue(Vector<uint8_t>&& data, uint8_t subtag);
size_t computeMemoryCost() const;
@@ -294,6 +300,9 @@ private:
// DenseArray fast path: array of primitives/strings/simple objects
FixedVector<DenseArrayElement> m_denseArrayElements {};
// TypedArray fast path: subtag identifying the TypedArray type (ArrayBufferViewSubtag)
uint8_t m_typedArraySubtag { 0 };
};
template<class Encoder>

View File

@@ -883,4 +883,253 @@ describe("Structured Clone Fast Path", () => {
port1.close();
port2.close();
});
// === TypedArray fast path tests ===
const typedArrayCtors = [
{ name: "Uint8Array", ctor: Uint8Array, values: [0, 1, 127, 255] },
{ name: "Int8Array", ctor: Int8Array, values: [-128, -1, 0, 1, 127] },
{ name: "Uint8ClampedArray", ctor: Uint8ClampedArray, values: [0, 1, 127, 255] },
{ name: "Uint16Array", ctor: Uint16Array, values: [0, 1, 256, 65535] },
{ name: "Int16Array", ctor: Int16Array, values: [-32768, -1, 0, 1, 32767] },
{ name: "Uint32Array", ctor: Uint32Array, values: [0, 1, 65536, 4294967295] },
{ name: "Int32Array", ctor: Int32Array, values: [-2147483648, -1, 0, 1, 2147483647] },
{ name: "Float32Array", ctor: Float32Array, values: [0, 1.5, -1.5, 3.4028234663852886e38] },
{ name: "Float64Array", ctor: Float64Array, values: [0, 1.5, -1.5, Number.MAX_VALUE, Number.MIN_VALUE] },
] as const;
for (const { name, ctor, values } of typedArrayCtors) {
test(`structuredClone(${name}) basic values`, () => {
const input = new ctor(values as any);
const cloned = structuredClone(input);
expect(cloned).toBeInstanceOf(ctor);
expect(cloned).toEqual(input);
expect(cloned.buffer).not.toBe(input.buffer);
});
}
test("structuredClone(BigInt64Array) basic values", () => {
const input = new BigInt64Array([-9223372036854775808n, -1n, 0n, 1n, 9223372036854775807n]);
const cloned = structuredClone(input);
expect(cloned).toBeInstanceOf(BigInt64Array);
expect(cloned).toEqual(input);
expect(cloned.buffer).not.toBe(input.buffer);
});
test("structuredClone(BigUint64Array) basic values", () => {
const input = new BigUint64Array([0n, 1n, 18446744073709551615n]);
const cloned = structuredClone(input);
expect(cloned).toBeInstanceOf(BigUint64Array);
expect(cloned).toEqual(input);
expect(cloned.buffer).not.toBe(input.buffer);
});
test("structuredClone(Float16Array) basic values", () => {
const input = new Float16Array([0, 1.5, -1.5, 65504]);
const cloned = structuredClone(input);
expect(cloned).toBeInstanceOf(Float16Array);
expect(cloned).toEqual(input);
expect(cloned.buffer).not.toBe(input.buffer);
});
test("structuredClone empty TypedArray", () => {
const input = new Uint8Array(0);
const cloned = structuredClone(input);
expect(cloned).toBeInstanceOf(Uint8Array);
expect(cloned.length).toBe(0);
expect(cloned.byteLength).toBe(0);
});
test("structuredClone large TypedArray (1MB)", () => {
const input = new Uint8Array(1024 * 1024);
for (let i = 0; i < input.length; i++) input[i] = i & 0xff;
const cloned = structuredClone(input);
expect(cloned).toBeInstanceOf(Uint8Array);
expect(cloned.length).toBe(input.length);
expect(cloned).toEqual(input);
expect(cloned.buffer).not.toBe(input.buffer);
});
test("structuredClone Float64Array with special values", () => {
const input = new Float64Array([NaN, Infinity, -Infinity, -0, 0]);
const cloned = structuredClone(input);
expect(cloned).toBeInstanceOf(Float64Array);
expect(cloned[0]).toBeNaN();
expect(cloned[1]).toBe(Infinity);
expect(cloned[2]).toBe(-Infinity);
expect(Object.is(cloned[3], -0)).toBe(true);
expect(cloned[4]).toBe(0);
});
test("structuredClone Float32Array with special values", () => {
const input = new Float32Array([NaN, Infinity, -Infinity, -0]);
const cloned = structuredClone(input);
expect(cloned).toBeInstanceOf(Float32Array);
expect(cloned[0]).toBeNaN();
expect(cloned[1]).toBe(Infinity);
expect(cloned[2]).toBe(-Infinity);
expect(Object.is(cloned[3], -0)).toBe(true);
});
test("structuredClone TypedArray creates independent copy", () => {
const input = new Uint8Array([1, 2, 3, 4, 5]);
const cloned = structuredClone(input);
cloned[0] = 255;
expect(input[0]).toBe(1);
input[1] = 200;
expect(cloned[1]).toBe(2);
});
test("structuredClone DataView falls back to slow path but works correctly", () => {
const buf = new ArrayBuffer(8);
const view = new DataView(buf);
view.setFloat64(0, 3.14);
const cloned = structuredClone(view);
expect(cloned).toBeInstanceOf(DataView);
expect(cloned.getFloat64(0)).toBe(3.14);
expect(cloned.buffer).not.toBe(buf);
});
test("structuredClone TypedArray slice view falls back to slow path", () => {
const buf = new ArrayBuffer(16);
new Uint8Array(buf).set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]);
const sliceView = new Uint8Array(buf, 4, 4);
const cloned = structuredClone(sliceView);
expect(cloned).toBeInstanceOf(Uint8Array);
expect(cloned).toEqual(new Uint8Array([4, 5, 6, 7]));
// structuredClone clones the full backing ArrayBuffer, preserving byteOffset
expect(cloned.byteOffset).toBe(4);
expect(cloned.buffer.byteLength).toBe(16);
});
test("structuredClone TypedArray with named properties falls back to slow path", () => {
const input = new Uint8Array([1, 2, 3]) as any;
input.customProp = "hello";
// Named properties on TypedArray are not cloneable via structuredClone,
// the slow path handles this correctly (ignores them)
const cloned = structuredClone(input);
expect(cloned).toBeInstanceOf(Uint8Array);
expect(cloned).toEqual(new Uint8Array([1, 2, 3]));
});
test("structuredClone detached TypedArray throws DataCloneError", () => {
const buf = new ArrayBuffer(8);
const input = new Uint8Array(buf);
// Detach the buffer by transferring it
structuredClone(buf, { transfer: [buf] });
expect(() => structuredClone(input)).toThrow();
});
test("postMessage TypedArray via MessageChannel", async () => {
const { port1, port2 } = new MessageChannel();
const input = new Uint8Array([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).toBeInstanceOf(Uint8Array);
expect(result).toEqual(input);
port1.close();
port2.close();
});
test("postMessage Float64Array via MessageChannel", async () => {
const { port1, port2 } = new MessageChannel();
const input = new Float64Array([1.1, 2.2, 3.3, NaN, Infinity]);
const { promise, resolve } = Promise.withResolvers();
port2.onmessage = (e: MessageEvent) => resolve(e.data);
port1.postMessage(input);
const result = await promise;
expect(result).toBeInstanceOf(Float64Array);
expect(result[0]).toBe(1.1);
expect(result[1]).toBe(2.2);
expect(result[2]).toBe(3.3);
expect(result[3]).toBeNaN();
expect(result[4]).toBe(Infinity);
port1.close();
port2.close();
});
test("postMessage BigInt64Array via MessageChannel", async () => {
const { port1, port2 } = new MessageChannel();
const input = new BigInt64Array([0n, -1n, 9223372036854775807n]);
const { promise, resolve } = Promise.withResolvers();
port2.onmessage = (e: MessageEvent) => resolve(e.data);
port1.postMessage(input);
const result = await promise;
expect(result).toBeInstanceOf(BigInt64Array);
expect(result).toEqual(input);
port1.close();
port2.close();
});
test("structuredClone TypedArray backed by SharedArrayBuffer falls back to slow path", () => {
const sab = new SharedArrayBuffer(16);
const view = new Uint8Array(sab);
view.set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
const cloned = structuredClone(view);
expect(cloned).toBeInstanceOf(Uint8Array);
expect(cloned).toEqual(view);
// The cloned view should NOT share memory with the original
expect(cloned.buffer).not.toBe(sab);
// Verify independence: modifying original doesn't affect clone
view[0] = 255;
expect(cloned[0]).toBe(1);
});
test("structuredClone Int32Array backed by SharedArrayBuffer preserves values", () => {
const sab = new SharedArrayBuffer(16);
const view = new Int32Array(sab);
view.set([100, 200, 300, 400]);
const cloned = structuredClone(view);
expect(cloned).toBeInstanceOf(Int32Array);
expect(cloned).toEqual(new Int32Array([100, 200, 300, 400]));
expect(cloned.buffer).not.toBe(sab);
});
test("postMessage TypedArray backed by SharedArrayBuffer via MessageChannel", async () => {
const { port1, port2 } = new MessageChannel();
const sab = new SharedArrayBuffer(8);
const input = new Uint8Array(sab);
input.set([10, 20, 30, 40, 50, 60, 70, 80]);
const { promise, resolve } = Promise.withResolvers();
port2.onmessage = (e: MessageEvent) => resolve(e.data);
port1.postMessage(input);
const result = await promise;
expect(result).toBeInstanceOf(Uint8Array);
expect(result).toEqual(input);
port1.close();
port2.close();
});
test("structuredClone partial-buffer TypedArray with byteOffset==0 preserves full buffer", () => {
// new Uint8Array(buf, 0, 8) over a 16-byte buffer: byteOffset is 0 but
// the view only covers the first half. The slow path clones the entire
// backing ArrayBuffer and preserves byteOffset/byteLength.
const buf = new ArrayBuffer(16);
const full = new Uint8Array(buf);
full.set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]);
const partial = new Uint8Array(buf, 0, 8);
const cloned = structuredClone(partial);
expect(cloned).toBeInstanceOf(Uint8Array);
expect(cloned).toEqual(new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]));
expect(cloned.byteOffset).toBe(0);
expect(cloned.byteLength).toBe(8);
// The cloned buffer must preserve the full backing ArrayBuffer size
expect(cloned.buffer.byteLength).toBe(16);
});
test("structuredClone partial-buffer Int32Array with byteOffset==0 preserves full buffer", () => {
const buf = new ArrayBuffer(32);
const partial = new Int32Array(buf, 0, 4); // 16 bytes out of 32
partial.set([100, 200, 300, 400]);
const cloned = structuredClone(partial);
expect(cloned).toBeInstanceOf(Int32Array);
expect(cloned).toEqual(new Int32Array([100, 200, 300, 400]));
expect(cloned.byteOffset).toBe(0);
expect(cloned.byteLength).toBe(16);
expect(cloned.buffer.byteLength).toBe(32);
});
});