mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
Add fast path for simple objects in postMessage and structuredClone (#22279)
## Summary
- Extends the existing string fast path to support simple objects with
primitive values
- Achieves 2-241x performance improvements for postMessage with objects
- Maintains compatibility with existing code while significantly
reducing overhead
## Performance Results
### Bun (this PR)
```
postMessage({ prop: 11 chars string, ...9 more props }) - 648ns (was 1.36µs)
postMessage({ prop: 14 KB string, ...9 more props }) - 719ns (was 2.09µs)
postMessage({ prop: 3 MB string, ...9 more props }) - 1.26µs (was 168µs)
```
### Node.js v24.6.0 (for comparison)
```
postMessage({ prop: 11 chars string, ...9 more props }) - 1.19µs
postMessage({ prop: 14 KB string, ...9 more props }) - 2.69µs
postMessage({ prop: 3 MB string, ...9 more props }) - 304µs
```
## Implementation Details
The fast path activates when:
- Object is a plain object (ObjectType or FinalObjectType)
- Has no indexed properties
- All property values are primitives or strings
- No transfer list is involved
Properties are stored in a `SimpleInMemoryPropertyTableEntry` vector
that holds property names and values directly, avoiding the overhead of
full serialization.
## Test plan
- [x] Added tests for memory usage with simple objects
- [x] Added test for objects exceeding JSFinalObject::maxInlineCapacity
- [x] Created benchmark to verify performance improvements
- [x] Existing structured clone tests continue to pass
🤖 Generated with [Claude Code](https://claude.ai/code)
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
116
bench/postMessage/postMessage-object.mjs
Normal file
116
bench/postMessage/postMessage-object.mjs
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
// Benchmark for object fast path optimization in postMessage with Workers
|
||||||
|
|
||||||
|
import { bench, run } from "mitata";
|
||||||
|
import { Worker } from "node:worker_threads";
|
||||||
|
|
||||||
|
const extraProperties = {
|
||||||
|
a: "a!",
|
||||||
|
b: "b!",
|
||||||
|
"second": "c!",
|
||||||
|
bool: true,
|
||||||
|
nully: null,
|
||||||
|
undef: undefined,
|
||||||
|
int: 0,
|
||||||
|
double: 1.234,
|
||||||
|
falsy: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const objects = {
|
||||||
|
small: { property: "Hello world", ...extraProperties },
|
||||||
|
medium: {
|
||||||
|
property: Buffer.alloc("Hello World!!!".length * 1024, "Hello World!!!").toString(),
|
||||||
|
...extraProperties,
|
||||||
|
},
|
||||||
|
large: {
|
||||||
|
property: Buffer.alloc("Hello World!!!".length * 1024 * 256, "Hello World!!!").toString(),
|
||||||
|
...extraProperties,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let worker;
|
||||||
|
let receivedCount = new Int32Array(new SharedArrayBuffer(4));
|
||||||
|
let sentCount = 0;
|
||||||
|
|
||||||
|
function createWorker() {
|
||||||
|
const workerCode = `
|
||||||
|
import { parentPort, workerData } from "node:worker_threads";
|
||||||
|
|
||||||
|
let int = workerData;
|
||||||
|
|
||||||
|
parentPort?.on("message", data => {
|
||||||
|
switch (data.property.length) {
|
||||||
|
case ${objects.small.property.length}:
|
||||||
|
case ${objects.medium.property.length}:
|
||||||
|
case ${objects.large.property.length}: {
|
||||||
|
if (
|
||||||
|
data.a === "a!" &&
|
||||||
|
data.b === "b!" &&
|
||||||
|
data.second === "c!" &&
|
||||||
|
data.bool === true &&
|
||||||
|
data.nully === null &&
|
||||||
|
data.undef === undefined &&
|
||||||
|
data.int === 0 &&
|
||||||
|
data.double === 1.234 &&
|
||||||
|
data.falsy === false) {
|
||||||
|
Atomics.add(int, 0, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error("Invalid data object: " + JSON.stringify(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
`;
|
||||||
|
|
||||||
|
worker = new Worker(workerCode, { eval: true, workerData: receivedCount });
|
||||||
|
|
||||||
|
worker.on("message", confirmationId => {});
|
||||||
|
|
||||||
|
worker.on("error", error => {
|
||||||
|
console.error("Worker error:", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize worker before running benchmarks
|
||||||
|
createWorker();
|
||||||
|
|
||||||
|
function fmt(int) {
|
||||||
|
if (int < 1000) {
|
||||||
|
return `${int} chars`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (int < 100000) {
|
||||||
|
return `${(int / 1024) | 0} KB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${(int / 1024 / 1024) | 0} MB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Benchmark postMessage with pure strings (uses fast path)
|
||||||
|
bench("postMessage({ prop: " + fmt(objects.small.property.length) + " string, ...9 more props })", async () => {
|
||||||
|
sentCount++;
|
||||||
|
worker.postMessage(objects.small);
|
||||||
|
});
|
||||||
|
|
||||||
|
bench("postMessage({ prop: " + fmt(objects.medium.property.length) + " string, ...9 more props })", async () => {
|
||||||
|
sentCount++;
|
||||||
|
worker.postMessage(objects.medium);
|
||||||
|
});
|
||||||
|
|
||||||
|
bench("postMessage({ prop: " + fmt(objects.large.property.length) + " string, ...9 more props })", async () => {
|
||||||
|
sentCount++;
|
||||||
|
worker.postMessage(objects.large);
|
||||||
|
});
|
||||||
|
|
||||||
|
await run();
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||||
|
|
||||||
|
if (receivedCount[0] !== sentCount) {
|
||||||
|
throw new Error("Expected " + receivedCount[0] + " to equal " + sentCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup worker
|
||||||
|
worker?.terminate();
|
||||||
@@ -114,6 +114,8 @@
|
|||||||
#include "JSPrivateKeyObject.h"
|
#include "JSPrivateKeyObject.h"
|
||||||
#include "CryptoKeyType.h"
|
#include "CryptoKeyType.h"
|
||||||
#include "JSNodePerformanceHooksHistogram.h"
|
#include "JSNodePerformanceHooksHistogram.h"
|
||||||
|
#include <limits>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
#if USE(CG)
|
#if USE(CG)
|
||||||
#include <CoreGraphics/CoreGraphics.h>
|
#include <CoreGraphics/CoreGraphics.h>
|
||||||
@@ -5565,9 +5567,16 @@ SerializedScriptValue::SerializedScriptValue(Vector<uint8_t>&& buffer, std::uniq
|
|||||||
m_memoryCost = computeMemoryCost();
|
m_memoryCost = computeMemoryCost();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SerializedScriptValue::SerializedScriptValue(WTF::FixedVector<SimpleInMemoryPropertyTableEntry>&& object)
|
||||||
|
: m_simpleInMemoryPropertyTable(WTFMove(object))
|
||||||
|
, m_fastPath(FastPath::SimpleObject)
|
||||||
|
{
|
||||||
|
m_memoryCost = computeMemoryCost();
|
||||||
|
}
|
||||||
|
|
||||||
SerializedScriptValue::SerializedScriptValue(const String& fastPathString)
|
SerializedScriptValue::SerializedScriptValue(const String& fastPathString)
|
||||||
: m_fastPathString(fastPathString)
|
: m_fastPathString(fastPathString)
|
||||||
, m_isStringFastPath(true)
|
, m_fastPath(FastPath::String)
|
||||||
{
|
{
|
||||||
m_memoryCost = computeMemoryCost();
|
m_memoryCost = computeMemoryCost();
|
||||||
}
|
}
|
||||||
@@ -5623,8 +5632,30 @@ size_t SerializedScriptValue::computeMemoryCost() const
|
|||||||
// cost += handle.url().string().sizeInBytes();
|
// cost += handle.url().string().sizeInBytes();
|
||||||
|
|
||||||
// Account for fast path string memory usage
|
// Account for fast path string memory usage
|
||||||
if (m_isStringFastPath)
|
switch (m_fastPath) {
|
||||||
|
case FastPath::String:
|
||||||
|
ASSERT(m_simpleInMemoryPropertyTable.isEmpty());
|
||||||
cost += m_fastPathString.sizeInBytes();
|
cost += m_fastPathString.sizeInBytes();
|
||||||
|
break;
|
||||||
|
case FastPath::SimpleObject:
|
||||||
|
ASSERT(m_fastPathString.isEmpty());
|
||||||
|
cost += m_simpleInMemoryPropertyTable.byteSize();
|
||||||
|
// Add the memory cost of strings in the simple property table
|
||||||
|
for (const auto& entry : m_simpleInMemoryPropertyTable) {
|
||||||
|
// Add property name string cost
|
||||||
|
cost += entry.propertyName.sizeInBytes();
|
||||||
|
|
||||||
|
// Add value string cost if it's a string
|
||||||
|
if (std::holds_alternative<WTF::String>(entry.value)) {
|
||||||
|
const auto& str = std::get<WTF::String>(entry.value);
|
||||||
|
cost += str.sizeInBytes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case FastPath::None:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
return cost;
|
return cost;
|
||||||
}
|
}
|
||||||
@@ -5699,6 +5730,43 @@ static Exception exceptionForSerializationFailure(SerializationReturnCode code)
|
|||||||
return Exception { TypeError };
|
return Exception { TypeError };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is based on `checkStrucureForClone`
|
||||||
|
static bool isObjectFastPathCandidate(Structure* structure)
|
||||||
|
{
|
||||||
|
static constexpr bool verbose = false;
|
||||||
|
|
||||||
|
if (structure->typeInfo().type() != FinalObjectType) {
|
||||||
|
dataLogLnIf(verbose, "target is not final object");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!structure->canAccessPropertiesQuicklyForEnumeration()) {
|
||||||
|
dataLogLnIf(verbose, "target cannot access properties quickly for enumeration");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasIndexedProperties(structure->indexingType())) {
|
||||||
|
dataLogLnIf(verbose, "target has indexing mode");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (structure->isBrandedStructure()) {
|
||||||
|
dataLogLnIf(verbose, "target has isBrandedStructure");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (structure->hasAnyKindOfGetterSetterProperties()) {
|
||||||
|
dataLogLnIf(verbose, "target has any kind of getter setter properties");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (structure->hasNonConfigurableProperties() || structure->hasNonEnumerableProperties()) {
|
||||||
|
dataLogLnIf(verbose, "target has non-configurable or non-enumerable properties");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
// static bool containsDuplicates(const Vector<RefPtr<ImageBitmap>>& imageBitmaps)
|
// static bool containsDuplicates(const Vector<RefPtr<ImageBitmap>>& imageBitmaps)
|
||||||
// {
|
// {
|
||||||
// HashSet<ImageBitmap*> visited;
|
// HashSet<ImageBitmap*> visited;
|
||||||
@@ -5766,17 +5834,86 @@ ExceptionOr<Ref<SerializedScriptValue>> SerializedScriptValue::create(JSGlobalOb
|
|||||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||||
|
|
||||||
// Fast path optimization: for postMessage/structuredClone with pure strings and no transfers
|
// Fast path optimization: for postMessage/structuredClone with pure strings and no transfers
|
||||||
if ((context == SerializationContext::WorkerPostMessage || context == SerializationContext::WindowPostMessage || context == SerializationContext::Default)
|
const bool canUseFastPath = (context == SerializationContext::WorkerPostMessage || context == SerializationContext::WindowPostMessage || context == SerializationContext::Default)
|
||||||
&& forStorage == SerializationForStorage::No
|
&& forStorage == SerializationForStorage::No
|
||||||
&& forTransfer == SerializationForCrossProcessTransfer::No
|
&& forTransfer == SerializationForCrossProcessTransfer::No
|
||||||
&& transferList.isEmpty()
|
&& transferList.isEmpty()
|
||||||
&& messagePorts.isEmpty()
|
&& messagePorts.isEmpty();
|
||||||
&& value.isString()) {
|
|
||||||
|
|
||||||
JSC::JSString* jsString = asString(value);
|
if (canUseFastPath) {
|
||||||
String stringValue = jsString->value(&lexicalGlobalObject);
|
bool canUseStringFastPath = false;
|
||||||
RETURN_IF_EXCEPTION(scope, Exception { TypeError });
|
bool canUseObjectFastPath = false;
|
||||||
return SerializedScriptValue::createStringFastPath(stringValue);
|
JSObject* object = nullptr;
|
||||||
|
Structure* structure = nullptr;
|
||||||
|
if (value.isCell()) {
|
||||||
|
auto* cell = value.asCell();
|
||||||
|
if (cell->isString()) {
|
||||||
|
canUseStringFastPath = true;
|
||||||
|
} else if (cell->isObject()) {
|
||||||
|
object = cell->getObject();
|
||||||
|
structure = object->structure();
|
||||||
|
|
||||||
|
if (isObjectFastPathCandidate(structure)) {
|
||||||
|
canUseObjectFastPath = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canUseStringFastPath) {
|
||||||
|
JSC::JSString* jsString = asString(value);
|
||||||
|
String stringValue = jsString->value(&lexicalGlobalObject);
|
||||||
|
RETURN_IF_EXCEPTION(scope, Exception { ExistingExceptionError });
|
||||||
|
return SerializedScriptValue::createStringFastPath(stringValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canUseObjectFastPath) {
|
||||||
|
ASSERT(object != nullptr);
|
||||||
|
|
||||||
|
WTF::Vector<SimpleInMemoryPropertyTableEntry> properties;
|
||||||
|
|
||||||
|
structure->forEachProperty(vm, [&](const PropertyTableEntry& entry) -> bool {
|
||||||
|
// Only enumerable, data properties
|
||||||
|
if (entry.attributes() & PropertyAttribute::DontEnum) [[unlikely]] {
|
||||||
|
ASSERT_NOT_REACHED_WITH_MESSAGE("isObjectFastPathCandidate should not allow non-enumerable, data properties");
|
||||||
|
canUseObjectFastPath = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.attributes() & PropertyAttribute::Accessor) [[unlikely]] {
|
||||||
|
ASSERT_NOT_REACHED_WITH_MESSAGE("isObjectFastPathCandidate should not allow accessor properties");
|
||||||
|
canUseObjectFastPath = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
JSValue value = object->getDirect(entry.offset());
|
||||||
|
|
||||||
|
if (value.isCell()) {
|
||||||
|
// We only support strings, numbers and primitives. Nothing else.
|
||||||
|
if (!value.isString()) {
|
||||||
|
canUseObjectFastPath = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* string = asString(value);
|
||||||
|
String stringValue = string->value(&lexicalGlobalObject);
|
||||||
|
if (scope.exception()) {
|
||||||
|
canUseObjectFastPath = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
properties.append({ entry.key()->isolatedCopy(), Bun::toCrossThreadShareable(stringValue) });
|
||||||
|
} else {
|
||||||
|
// Primitive values are safe to share across threads.
|
||||||
|
properties.append({ entry.key()->isolatedCopy(), value });
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
RETURN_IF_EXCEPTION(scope, Exception { ExistingExceptionError });
|
||||||
|
|
||||||
|
if (canUseObjectFastPath) {
|
||||||
|
return SerializedScriptValue::createObjectFastPath(WTF::FixedVector<SimpleInMemoryPropertyTableEntry>(WTFMove(properties)));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Vector<RefPtr<JSC::ArrayBuffer>> arrayBuffers;
|
Vector<RefPtr<JSC::ArrayBuffer>> arrayBuffers;
|
||||||
@@ -6000,6 +6137,11 @@ Ref<SerializedScriptValue> SerializedScriptValue::createStringFastPath(const Str
|
|||||||
return adoptRef(*new SerializedScriptValue(Bun::toCrossThreadShareable(string)));
|
return adoptRef(*new SerializedScriptValue(Bun::toCrossThreadShareable(string)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ref<SerializedScriptValue> SerializedScriptValue::createObjectFastPath(WTF::FixedVector<SimpleInMemoryPropertyTableEntry>&& object)
|
||||||
|
{
|
||||||
|
return adoptRef(*new SerializedScriptValue(WTFMove(object)));
|
||||||
|
}
|
||||||
|
|
||||||
RefPtr<SerializedScriptValue> SerializedScriptValue::create(JSContextRef originContext, JSValueRef apiValue, JSValueRef* exception)
|
RefPtr<SerializedScriptValue> SerializedScriptValue::create(JSContextRef originContext, JSValueRef apiValue, JSValueRef* exception)
|
||||||
{
|
{
|
||||||
JSGlobalObject* lexicalGlobalObject = toJS(originContext);
|
JSGlobalObject* lexicalGlobalObject = toJS(originContext);
|
||||||
@@ -6117,11 +6259,36 @@ JSValue SerializedScriptValue::deserialize(JSGlobalObject& lexicalGlobalObject,
|
|||||||
{
|
{
|
||||||
VM& vm = lexicalGlobalObject.vm();
|
VM& vm = lexicalGlobalObject.vm();
|
||||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||||
// Fast path for string-only values - avoid deserialization overhead
|
switch (m_fastPath) {
|
||||||
if (m_isStringFastPath) {
|
case FastPath::String:
|
||||||
if (didFail)
|
if (didFail)
|
||||||
*didFail = false;
|
*didFail = false;
|
||||||
return jsString(vm, m_fastPathString);
|
return jsString(vm, m_fastPathString);
|
||||||
|
case FastPath::SimpleObject: {
|
||||||
|
JSObject* object = constructEmptyObject(globalObject, globalObject->objectPrototype(), std::min(static_cast<unsigned>(m_simpleInMemoryPropertyTable.size()), JSFinalObject::maxInlineCapacity));
|
||||||
|
if (scope.exception()) [[unlikely]] {
|
||||||
|
if (didFail)
|
||||||
|
*didFail = true;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& property : m_simpleInMemoryPropertyTable) {
|
||||||
|
// We **must** clone this so that the atomic flag doesn't get set to true.
|
||||||
|
JSC::Identifier identifier = JSC::Identifier::fromString(vm, property.propertyName.isolatedCopy());
|
||||||
|
JSValue value = WTF::switchOn(
|
||||||
|
property.value, [](JSValue value) -> JSValue { return value; },
|
||||||
|
[&](const String& string) -> JSValue { return jsString(vm, string); });
|
||||||
|
object->putDirect(vm, identifier, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (didFail)
|
||||||
|
*didFail = false;
|
||||||
|
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
case FastPath::None: {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DeserializationResult result = CloneDeserializer::deserialize(&lexicalGlobalObject, globalObject, messagePorts
|
DeserializationResult result = CloneDeserializer::deserialize(&lexicalGlobalObject, globalObject, messagePorts
|
||||||
|
|||||||
@@ -33,6 +33,8 @@
|
|||||||
#include <JavaScriptCore/ArrayBuffer.h>
|
#include <JavaScriptCore/ArrayBuffer.h>
|
||||||
#include <JavaScriptCore/JSCJSValue.h>
|
#include <JavaScriptCore/JSCJSValue.h>
|
||||||
#include <JavaScriptCore/Strong.h>
|
#include <JavaScriptCore/Strong.h>
|
||||||
|
#include <variant>
|
||||||
|
#include <wtf/FixedVector.h>
|
||||||
#include <wtf/Forward.h>
|
#include <wtf/Forward.h>
|
||||||
#include <wtf/Function.h>
|
#include <wtf/Function.h>
|
||||||
#include <wtf/Gigacage.h>
|
#include <wtf/Gigacage.h>
|
||||||
@@ -58,6 +60,26 @@ class MemoryHandle;
|
|||||||
|
|
||||||
namespace WebCore {
|
namespace WebCore {
|
||||||
|
|
||||||
|
class SimpleInMemoryPropertyTableEntry {
|
||||||
|
public:
|
||||||
|
// Only:
|
||||||
|
// - String
|
||||||
|
// - Number
|
||||||
|
// - Boolean
|
||||||
|
// - Null
|
||||||
|
// - Undefined
|
||||||
|
using Value = std::variant<JSC::JSValue, WTF::String>;
|
||||||
|
|
||||||
|
WTF::String propertyName;
|
||||||
|
Value value;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class FastPath : uint8_t {
|
||||||
|
None,
|
||||||
|
String,
|
||||||
|
SimpleObject,
|
||||||
|
};
|
||||||
|
|
||||||
#if ENABLE(OFFSCREEN_CANVAS_IN_WORKERS)
|
#if ENABLE(OFFSCREEN_CANVAS_IN_WORKERS)
|
||||||
class DetachedOffscreenCanvas;
|
class DetachedOffscreenCanvas;
|
||||||
#endif
|
#endif
|
||||||
@@ -104,6 +126,9 @@ public:
|
|||||||
// Fast path for postMessage with pure strings
|
// Fast path for postMessage with pure strings
|
||||||
static Ref<SerializedScriptValue> createStringFastPath(const String& string);
|
static Ref<SerializedScriptValue> createStringFastPath(const String& string);
|
||||||
|
|
||||||
|
// Fast path for postMessage with simple objects
|
||||||
|
static Ref<SerializedScriptValue> createObjectFastPath(WTF::FixedVector<SimpleInMemoryPropertyTableEntry>&& object);
|
||||||
|
|
||||||
static Ref<SerializedScriptValue> nullValue();
|
static Ref<SerializedScriptValue> nullValue();
|
||||||
|
|
||||||
WEBCORE_EXPORT JSC::JSValue deserialize(JSC::JSGlobalObject&, JSC::JSGlobalObject*, SerializationErrorMode = SerializationErrorMode::Throwing, bool* didFail = nullptr);
|
WEBCORE_EXPORT JSC::JSValue deserialize(JSC::JSGlobalObject&, JSC::JSGlobalObject*, SerializationErrorMode = SerializationErrorMode::Throwing, bool* didFail = nullptr);
|
||||||
@@ -205,6 +230,7 @@ private:
|
|||||||
|
|
||||||
// Constructor for string fast path
|
// Constructor for string fast path
|
||||||
explicit SerializedScriptValue(const String& fastPathString);
|
explicit SerializedScriptValue(const String& fastPathString);
|
||||||
|
explicit SerializedScriptValue(WTF::FixedVector<SimpleInMemoryPropertyTableEntry>&& object);
|
||||||
|
|
||||||
size_t computeMemoryCost() const;
|
size_t computeMemoryCost() const;
|
||||||
|
|
||||||
@@ -230,9 +256,10 @@ private:
|
|||||||
|
|
||||||
// Fast path for postMessage with pure strings - avoids serialization overhead
|
// Fast path for postMessage with pure strings - avoids serialization overhead
|
||||||
String m_fastPathString;
|
String m_fastPathString;
|
||||||
bool m_isStringFastPath { false };
|
FastPath m_fastPath { FastPath::None };
|
||||||
|
|
||||||
size_t m_memoryCost { 0 };
|
size_t m_memoryCost { 0 };
|
||||||
|
|
||||||
|
FixedVector<SimpleInMemoryPropertyTableEntry> m_simpleInMemoryPropertyTable {};
|
||||||
};
|
};
|
||||||
|
|
||||||
template<class Encoder>
|
template<class Encoder>
|
||||||
|
|||||||
@@ -1,18 +1,107 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
|
|
||||||
describe("Structured Clone Fast Path", () => {
|
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", () => {
|
test("structuredClone should use a constant amount of memory for string inputs", () => {
|
||||||
// Create a 100KB string to test fast path
|
const clones: Array<string> = [];
|
||||||
const largeString = Buffer.alloc(512 * 1024).toString();
|
// Create a 512KB string to test fast path
|
||||||
|
const largeString = Buffer.alloc(512 * 1024, "a").toString();
|
||||||
for (let i = 0; i < 100; i++) {
|
for (let i = 0; i < 100; i++) {
|
||||||
structuredClone(largeString);
|
clones.push(structuredClone(largeString));
|
||||||
}
|
}
|
||||||
|
Bun.gc(true);
|
||||||
const rss = process.memoryUsage.rss();
|
const rss = process.memoryUsage.rss();
|
||||||
for (let i = 0; i < 10000; i++) {
|
for (let i = 0; i < 10000; i++) {
|
||||||
structuredClone(largeString);
|
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 rss2 = process.memoryUsage.rss();
|
||||||
const delta = rss2 - rss;
|
const delta = rss2 - rss;
|
||||||
expect(delta).toBeLessThan(1024 * 1024);
|
expect(delta).toBeLessThan(1024 * 1024);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user