mirror of
https://github.com/oven-sh/bun
synced 2026-02-16 22:01:47 +00:00
Compare commits
5 Commits
claude/fix
...
claude/str
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e125b725d | ||
|
|
72e1c6577c | ||
|
|
3cb5c4a722 | ||
|
|
30ca7c952b | ||
|
|
c153188363 |
68
bench/snippets/structuredClone-typedarray.mjs
Normal file
68
bench/snippets/structuredClone-typedarray.mjs
Normal 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();
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user