diff --git a/bench/string-fastpath.mjs b/bench/string-fastpath.mjs new file mode 100644 index 0000000000..0f3f216ffa --- /dev/null +++ b/bench/string-fastpath.mjs @@ -0,0 +1,56 @@ +// Benchmark for string fast path optimization in postMessage and structuredClone + +import { bench, run } from "mitata"; + +// Test strings of different sizes +const strings = { + small: "Hello world", + medium: "Hello World!!!".repeat(1024).split("").join(""), + large: "Hello World!!!".repeat(1024).repeat(1024).split("").join(""), +}; + +console.log("String fast path benchmark"); +console.log("Comparing pure strings (fast path) vs objects containing strings (traditional)"); +console.log("For structuredClone, pure strings should have constant time regardless of size."); +console.log(""); + +// Benchmark structuredClone with pure strings (uses fast path) +bench("structuredClone small string (fast path)", () => { + structuredClone(strings.small); +}); + +bench("structuredClone medium string (fast path)", () => { + structuredClone(strings.medium); +}); + +bench("structuredClone large string (fast path)", () => { + structuredClone(strings.large); +}); + +// Benchmark structuredClone with objects containing strings (traditional path) +bench("structuredClone object with small string", () => { + structuredClone({ str: strings.small }); +}); + +bench("structuredClone object with medium string", () => { + structuredClone({ str: strings.medium }); +}); + +bench("structuredClone object with large string", () => { + structuredClone({ str: strings.large }); +}); + +// Multiple string cloning benchmark +bench("structuredClone 100 small strings", () => { + for (let i = 0; i < 100; i++) { + structuredClone(strings.small); + } +}); + +bench("structuredClone 100 small objects", () => { + for (let i = 0; i < 100; i++) { + structuredClone({ str: strings.small }); + } +}); + +await run(); diff --git a/bench/string-postmessage.mjs b/bench/string-postmessage.mjs new file mode 100644 index 0000000000..9832e531ee --- /dev/null +++ b/bench/string-postmessage.mjs @@ -0,0 +1,77 @@ +// Benchmark for string fast path optimization in postMessage with Workers + +import { bench, run } from "mitata"; +import { Worker, isMainThread, parentPort } from "node:worker_threads"; + +// Test strings of different sizes +const strings = { + small: "Hello world", + medium: Buffer.alloc("Hello World!!!".length * 1024, "Hello World!!!").toString(), + large: Buffer.alloc("Hello World!!!".length * 1024 * 256, "Hello World!!!").toString(), +}; + +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 => { + Atomics.add(int, 0, 1); + }); + `; + + 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(" + fmt(strings.small.length) + " string)", async () => { + sentCount++; + worker.postMessage(strings.small); +}); + +bench("postMessage(" + fmt(strings.medium.length) + " string)", async () => { + sentCount++; + worker.postMessage(strings.medium); +}); + +bench("postMessage(" + fmt(strings.large.length) + " string)", async () => { + sentCount++; + worker.postMessage(strings.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(); diff --git a/cmake/tools/SetupWebKit.cmake b/cmake/tools/SetupWebKit.cmake index 157cb14b9e..da27582afb 100644 --- a/cmake/tools/SetupWebKit.cmake +++ b/cmake/tools/SetupWebKit.cmake @@ -2,7 +2,7 @@ option(WEBKIT_VERSION "The version of WebKit to use") option(WEBKIT_LOCAL "If a local version of WebKit should be used instead of downloading") if(NOT WEBKIT_VERSION) - set(WEBKIT_VERSION aa4997abc9126f5a7557c9ecb7e8104779d87ec4) + set(WEBKIT_VERSION 53385bda2d2270223ac66f7b021a4aec3dd6df75) endif() string(SUBSTRING ${WEBKIT_VERSION} 0 16 WEBKIT_VERSION_PREFIX) diff --git a/src/bun.js/api/JSBundler.zig b/src/bun.js/api/JSBundler.zig index f4505172b5..4ffa0941d2 100644 --- a/src/bun.js/api/JSBundler.zig +++ b/src/bun.js/api/JSBundler.zig @@ -521,7 +521,7 @@ pub const JSBundler = struct { } var plugins: ?*Plugin = null; - const config = try Config.fromJS(globalThis, arguments[0], &plugins, globalThis.allocator()); + const config = try Config.fromJS(globalThis, arguments[0], &plugins, bun.default_allocator); return bun.BundleV2.generateFromJavaScript( config, diff --git a/src/bun.js/api/sockets.classes.ts b/src/bun.js/api/sockets.classes.ts index 95500fbe9e..833651452e 100644 --- a/src/bun.js/api/sockets.classes.ts +++ b/src/bun.js/api/sockets.classes.ts @@ -459,7 +459,7 @@ export default [ finalize: true, estimatedSize: true, // inspectCustom: true, - structuredClone: { transferable: false, tag: 251 }, + structuredClone: { transferable: false, tag: 251, storable: false }, JSType: "0b11101110", klass: { isBlockList: { diff --git a/src/bun.js/api/sourcemap.classes.ts b/src/bun.js/api/sourcemap.classes.ts index 9a4ebd5201..0b97621472 100644 --- a/src/bun.js/api/sourcemap.classes.ts +++ b/src/bun.js/api/sourcemap.classes.ts @@ -27,6 +27,5 @@ export default [ constructNeedsThis: true, memoryCost: true, estimatedSize: true, - structuredClone: false, }), ]; diff --git a/src/bun.js/bindings/BunString.cpp b/src/bun.js/bindings/BunString.cpp index 8b89b27a0b..0106f02d3c 100644 --- a/src/bun.js/bindings/BunString.cpp +++ b/src/bun.js/bindings/BunString.cpp @@ -1,5 +1,6 @@ +#include "BunString.h" #include "helpers.h" #include "root.h" #include "headers-handwritten.h" @@ -279,6 +280,63 @@ BunString toStringView(StringView view) }; } +bool isCrossThreadShareable(const WTF::String& string) +{ + if (!string.impl()) + return false; + + auto* impl = string.impl(); + + // 1) Never share AtomStringImpl/symbols - they have special thread-unsafe behavior + if (impl->isAtom() || impl->isSymbol()) + return false; + + // 2) Don't share slices + if (impl->bufferOwnership() == StringImpl::BufferSubstring) + return false; + + return true; +} + +Ref toCrossThreadShareable(Ref impl) +{ + if (impl->isAtom() || impl->isSymbol()) + return impl->isolatedCopy(); + + if (impl->bufferOwnership() == StringImpl::BufferSubstring) + return impl->isolatedCopy(); + + // 3) Ensure we won't lazily touch hash/flags on the consumer thread + // Force hash computation on this thread before sharing + impl->hash(); + impl->setNeverAtomize(); + + return impl; +} + +WTF::String toCrossThreadShareable(const WTF::String& string) +{ + if (!string.impl()) + return string; + + auto* impl = string.impl(); + + // 1) Never share AtomStringImpl/symbols - they have special thread-unsafe behavior + if (impl->isAtom() || impl->isSymbol()) + return string.isolatedCopy(); + + // 2) Don't share slices + if (impl->bufferOwnership() == StringImpl::BufferSubstring) + return string.isolatedCopy(); + + // 3) Ensure we won't lazily touch hash/flags on the consumer thread + // Force hash computation on this thread before sharing + const_cast(impl)->hash(); + const_cast(impl)->setNeverAtomize(); + + return string; +} + } extern "C" JSC::EncodedJSValue BunString__toJS(JSC::JSGlobalObject* globalObject, const BunString* bunString) diff --git a/src/bun.js/bindings/BunString.h b/src/bun.js/bindings/BunString.h index 883bb9c40b..e03a8a9272 100644 --- a/src/bun.js/bindings/BunString.h +++ b/src/bun.js/bindings/BunString.h @@ -55,4 +55,9 @@ public: return std::span(reinterpret_cast(m_view.span8().data()), m_view.length()); } }; + +bool isCrossThreadShareable(const WTF::String& string); +WTF::String toCrossThreadShareable(const WTF::String& string); +Ref toCrossThreadShareable(Ref impl); + } diff --git a/src/bun.js/bindings/IPC.cpp b/src/bun.js/bindings/IPC.cpp index d5641de572..e4a60f6015 100644 --- a/src/bun.js/bindings/IPC.cpp +++ b/src/bun.js/bindings/IPC.cpp @@ -2,12 +2,13 @@ #include "headers-handwritten.h" #include "BunBuiltinNames.h" #include "WebCoreJSBuiltins.h" +#include "ZigGlobalObject.h" -extern "C" [[ZIG_EXPORT(zero_is_throw)]] JSC::EncodedJSValue IPCSerialize(JSC::JSGlobalObject* global, JSC::EncodedJSValue message, JSC::EncodedJSValue handle) +extern "C" [[ZIG_EXPORT(zero_is_throw)]] JSC::EncodedJSValue IPCSerialize(Zig::GlobalObject* global, JSC::EncodedJSValue message, JSC::EncodedJSValue handle) { auto& vm = JSC::getVM(global); auto scope = DECLARE_THROW_SCOPE(vm); - JSC::JSFunction* serializeFunction = JSC::JSFunction::create(vm, global, WebCore::ipcSerializeCodeGenerator(vm), global); + JSC::JSFunction* serializeFunction = global->m_ipcSerializeFunction.getInitializedOnMainThread(global); JSC::CallData callData = JSC::getCallData(serializeFunction); JSC::MarkedArgumentBuffer args; @@ -19,11 +20,11 @@ extern "C" [[ZIG_EXPORT(zero_is_throw)]] JSC::EncodedJSValue IPCSerialize(JSC::J return JSC::JSValue::encode(result); } -extern "C" [[ZIG_EXPORT(zero_is_throw)]] JSC::EncodedJSValue IPCParse(JSC::JSGlobalObject* global, JSC::EncodedJSValue target, JSC::EncodedJSValue serialized, JSC::EncodedJSValue fd) +extern "C" [[ZIG_EXPORT(zero_is_throw)]] JSC::EncodedJSValue IPCParse(Zig::GlobalObject* global, JSC::EncodedJSValue target, JSC::EncodedJSValue serialized, JSC::EncodedJSValue fd) { auto& vm = JSC::getVM(global); auto scope = DECLARE_THROW_SCOPE(vm); - JSC::JSFunction* parseFunction = JSC::JSFunction::create(vm, global, WebCore::ipcParseHandleCodeGenerator(vm), global); + JSC::JSFunction* parseFunction = global->m_ipcParseHandleFunction.getInitializedOnMainThread(global); JSC::CallData callData = JSC::getCallData(parseFunction); JSC::MarkedArgumentBuffer args; diff --git a/src/bun.js/bindings/JSValue.zig b/src/bun.js/bindings/JSValue.zig index 21f6ab12e2..99660bc601 100644 --- a/src/bun.js/bindings/JSValue.zig +++ b/src/bun.js/bindings/JSValue.zig @@ -2168,7 +2168,7 @@ pub const JSValue = enum(i64) { return bun.jsc.fromJSHostCall(global, @src(), Bun__JSValue__deserialize, .{ global, bytes.ptr, bytes.len }); } - extern fn Bun__serializeJSValue(global: *jsc.JSGlobalObject, value: JSValue, forTransfer: bool) SerializedScriptValue.External; + extern fn Bun__serializeJSValue(global: *jsc.JSGlobalObject, value: JSValue, flags: u8) SerializedScriptValue.External; extern fn Bun__SerializedScriptSlice__free(*anyopaque) void; pub const SerializedScriptValue = struct { @@ -2186,10 +2186,20 @@ pub const JSValue = enum(i64) { } }; + pub const SerializedFlags = packed struct(u8) { + forCrossProcessTransfer: bool = false, + forStorage: bool = false, + _padding: u6 = 0, + }; + /// Throws a JS exception and returns null if the serialization fails, otherwise returns a SerializedScriptValue. /// Must be freed when you are done with the bytes. - pub inline fn serialize(this: JSValue, global: *JSGlobalObject, forTransfer: bool) bun.JSError!SerializedScriptValue { - const value = try bun.jsc.fromJSHostCallGeneric(global, @src(), Bun__serializeJSValue, .{ global, this, forTransfer }); + pub inline fn serialize(this: JSValue, global: *JSGlobalObject, flags: SerializedFlags) bun.JSError!SerializedScriptValue { + var flags_u8: u8 = 0; + if (flags.forCrossProcessTransfer) flags_u8 |= 1 << 0; + if (flags.forStorage) flags_u8 |= 1 << 1; + + const value = try bun.jsc.fromJSHostCallGeneric(global, @src(), Bun__serializeJSValue, .{ global, this, flags_u8 }); return .{ .data = value.bytes.?[0..value.size], .handle = value.handle.? }; } diff --git a/src/bun.js/bindings/Serialization.cpp b/src/bun.js/bindings/Serialization.cpp index 6d51c87ab9..9af0d6a492 100644 --- a/src/bun.js/bindings/Serialization.cpp +++ b/src/bun.js/bindings/Serialization.cpp @@ -15,8 +15,14 @@ struct SerializedValueSlice { WebCore::SerializedScriptValue* value; // NOLINT }; +enum class SerializedFlags : uint8_t { + None = 0, + ForCrossProcessTransfer = 1 << 0, + ForStorage = 1 << 1, +}; + /// Returns a "slice" that also contains a pointer to the SerializedScriptValue. Must be freed by the caller -extern "C" SerializedValueSlice Bun__serializeJSValue(JSGlobalObject* globalObject, EncodedJSValue encodedValue, bool forTransferBool) +extern "C" SerializedValueSlice Bun__serializeJSValue(JSGlobalObject* globalObject, EncodedJSValue encodedValue, const SerializedFlags flags) { auto& vm = JSC::getVM(globalObject); auto scope = DECLARE_THROW_SCOPE(vm); @@ -24,9 +30,9 @@ extern "C" SerializedValueSlice Bun__serializeJSValue(JSGlobalObject* globalObje Vector> transferList; Vector> dummyPorts; - auto forStorage = SerializationForStorage::No; + auto forStorage = (static_cast(flags) & static_cast(SerializedFlags::ForStorage)) ? SerializationForStorage::Yes : SerializationForStorage::No; auto context = SerializationContext::Default; - auto forTransferEnum = forTransferBool ? SerializationForTransfer::Yes : SerializationForTransfer::No; + auto forTransferEnum = (static_cast(flags) & static_cast(SerializedFlags::ForCrossProcessTransfer)) ? SerializationForCrossProcessTransfer::Yes : SerializationForCrossProcessTransfer::No; ExceptionOr> serialized = SerializedScriptValue::create(*globalObject, value, WTFMove(transferList), dummyPorts, forStorage, context, forTransferEnum); EXCEPTION_ASSERT(!!scope.exception() == serialized.hasException()); diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index f8143ec8fe..16cd139166 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -1566,54 +1566,6 @@ JSC_DEFINE_HOST_FUNCTION(functionNativeMicrotaskTrampoline, return JSValue::encode(jsUndefined()); } -JSC_DEFINE_HOST_FUNCTION(functionStructuredClone, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) -{ - auto& vm = JSC::getVM(globalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); - - if (callFrame->argumentCount() == 0) { - throwTypeError(globalObject, throwScope, "structuredClone requires 1 argument"_s); - return {}; - } - - JSC::JSValue value = callFrame->argument(0); - JSC::JSValue options = callFrame->argument(1); - - Vector> transferList; - - if (options.isObject()) { - JSC::JSObject* optionsObject = options.getObject(); - JSC::JSValue transferListValue = optionsObject->get(globalObject, vm.propertyNames->transfer); - RETURN_IF_EXCEPTION(throwScope, {}); - if (transferListValue.isObject()) { - JSC::JSObject* transferListObject = transferListValue.getObject(); - if (auto* transferListArray = jsDynamicCast(transferListObject)) { - for (unsigned i = 0; i < transferListArray->length(); i++) { - JSC::JSValue transferListValue = transferListArray->get(globalObject, i); - RETURN_IF_EXCEPTION(throwScope, {}); - if (transferListValue.isObject()) { - JSC::JSObject* transferListObject = transferListValue.getObject(); - transferList.append(JSC::Strong(vm, transferListObject)); - } - } - } - } - } - - Vector> ports; - ExceptionOr> serialized = SerializedScriptValue::create(*globalObject, value, WTFMove(transferList), ports); - if (serialized.hasException()) { - WebCore::propagateException(*globalObject, throwScope, serialized.releaseException()); - RELEASE_AND_RETURN(throwScope, {}); - } - throwScope.assertNoException(); - - JSValue deserialized = serialized.releaseReturnValue()->deserialize(*globalObject, globalObject, ports); - RETURN_IF_EXCEPTION(throwScope, {}); - - return JSValue::encode(deserialized); -} - JSC_DEFINE_HOST_FUNCTION(functionBTOA, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { @@ -3375,6 +3327,14 @@ void GlobalObject::finishCreation(VM& vm) init.set(AsyncContextFrame::createStructure(init.vm, init.owner)); }); + m_ipcParseHandleFunction.initLater([](const LazyProperty::Initializer& init) { + init.set(JSC::JSFunction::create(init.vm, init.owner, WebCore::ipcParseHandleCodeGenerator(init.vm), init.owner)); + }); + + m_ipcSerializeFunction.initLater([](const LazyProperty::Initializer& init) { + init.set(JSC::JSFunction::create(init.vm, init.owner, WebCore::ipcSerializeCodeGenerator(init.vm), init.owner)); + }); + m_JSFileSinkClassStructure.initLater( [](LazyClassStructure::Initializer& init) { auto* prototype = createJSSinkPrototype(init.vm, init.global, WebCore::SinkID::FileSink); @@ -3898,6 +3858,25 @@ uint8_t GlobalObject::drainMicrotasks() { auto& vm = this->vm(); auto scope = DECLARE_CATCH_SCOPE(vm); + + if (auto* exception = scope.exception()) [[unlikely]] { + if (vm.isTerminationException(exception)) [[unlikely]] { + return 1; + } + +#if ASSERT_ENABLED + scope.clearException(); + // We should not have an exception here. + // But it's an easy mistake to make. + // Let's log it so that we can debug this. + Bun__reportError(this, JSValue::encode(exception)); + + // And re-throw it to preserve the production behavior. + auto throwScope = DECLARE_THROW_SCOPE(vm); + throwScope.throwException(this, exception); + throwScope.release(); +#endif + } scope.assertNoExceptionExceptTermination(); if (auto nextTickQueue = this->m_nextTickQueue.get()) { diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h index b9774e4f24..545f9bdd32 100644 --- a/src/bun.js/bindings/ZigGlobalObject.h +++ b/src/bun.js/bindings/ZigGlobalObject.h @@ -627,7 +627,9 @@ public: V(public, LazyPropertyOfGlobalObject, m_statFsValues) \ V(public, LazyPropertyOfGlobalObject, m_bigintStatFsValues) \ V(public, LazyPropertyOfGlobalObject, m_nodeVMDontContextify) \ - V(public, LazyPropertyOfGlobalObject, m_nodeVMUseMainContextDefaultLoader) + V(public, LazyPropertyOfGlobalObject, m_nodeVMUseMainContextDefaultLoader) \ + V(public, LazyPropertyOfGlobalObject, m_ipcSerializeFunction) \ + V(public, LazyPropertyOfGlobalObject, m_ipcParseHandleFunction) #define DECLARE_GLOBALOBJECT_GC_MEMBER(visibility, T, name) \ visibility: \ diff --git a/src/bun.js/bindings/ZigGlobalObject.lut.txt b/src/bun.js/bindings/ZigGlobalObject.lut.txt index c28969dd5a..49b22f1412 100644 --- a/src/bun.js/bindings/ZigGlobalObject.lut.txt +++ b/src/bun.js/bindings/ZigGlobalObject.lut.txt @@ -20,7 +20,7 @@ setImmediate functionSetImmediate Function 1 setInterval functionSetInterval Function 1 setTimeout functionSetTimeout Function 1 - structuredClone functionStructuredClone Function 2 + structuredClone WebCore::jsFunctionStructuredClone Function 2 global GlobalObject_getGlobalThis PropertyCallback diff --git a/src/bun.js/bindings/webcore/SerializedScriptValue.cpp b/src/bun.js/bindings/webcore/SerializedScriptValue.cpp index 638464c3f9..f5bcd71990 100644 --- a/src/bun.js/bindings/webcore/SerializedScriptValue.cpp +++ b/src/bun.js/bindings/webcore/SerializedScriptValue.cpp @@ -26,7 +26,7 @@ #include "config.h" #include "SerializedScriptValue.h" - +#include "BunString.h" // #include "BlobRegistry.h" // #include "ByteArrayPixelBuffer.h" #include "CryptoKeyAES.h" @@ -890,7 +890,7 @@ public: WasmMemoryHandleArray& wasmMemoryHandles, #endif Vector& out, SerializationContext context, ArrayBufferContentsArray& sharedBuffers, - SerializationForStorage forStorage, SerializationForTransfer forTransfer) + SerializationForStorage forStorage, SerializationForCrossProcessTransfer forTransfer) { CloneSerializer serializer(lexicalGlobalObject, messagePorts, arrayBuffers, #if ENABLE(OFFSCREEN_CANVAS_IN_WORKERS) @@ -992,7 +992,7 @@ private: WasmModuleArray& wasmModules, WasmMemoryHandleArray& wasmMemoryHandles, #endif - Vector& out, SerializationContext context, ArrayBufferContentsArray& sharedBuffers, SerializationForStorage forStorage, SerializationForTransfer forTransfer) + Vector& out, SerializationContext context, ArrayBufferContentsArray& sharedBuffers, SerializationForStorage forStorage, SerializationForCrossProcessTransfer forTransfer) : CloneBase(lexicalGlobalObject) , m_buffer(out) , m_emptyIdentifier(Identifier::fromString(lexicalGlobalObject->vm(), emptyString())) @@ -1948,15 +1948,19 @@ private: #endif // write bun types - if (auto _cloneable = StructuredCloneableSerialize::fromJS(value)) { - if (m_forTransfer == SerializationForTransfer::Yes && !SerializedScriptValue::isTransferable(m_lexicalGlobalObject, value)) { + auto _cloneable = StructuredCloneableSerialize::fromJS(value); + if (_cloneable) { + auto cloneable = _cloneable.value(); + const bool isTransferCompatible = m_forTransfer == SerializationForCrossProcessTransfer::Yes ? cloneable.isForTransfer : true; + const bool isStorageCompatible = m_forStorage == SerializationForStorage::Yes ? cloneable.isForStorage : true; + if (!isTransferCompatible || !isStorageCompatible) { write(ObjectTag); write(TerminatorTag); return true; } - StructuredCloneableSerialize cloneable = WTFMove(_cloneable.value()); - write(cloneable.tag); - cloneable.write(this, m_lexicalGlobalObject); + StructuredCloneableSerialize to_write = WTFMove(_cloneable.value()); + write(to_write.tag); + to_write.write(this, m_lexicalGlobalObject); return true; } @@ -2559,7 +2563,7 @@ private: Vector>& m_serializedVideoFrames; #endif SerializationForStorage m_forStorage; - SerializationForTransfer m_forTransfer; + SerializationForCrossProcessTransfer m_forTransfer; }; SYSV_ABI void SerializedScriptValue::writeBytesForBun(CloneSerializer* ctx, const uint8_t* data, uint32_t size) @@ -5561,6 +5565,13 @@ SerializedScriptValue::SerializedScriptValue(Vector&& buffer, std::uniq m_memoryCost = computeMemoryCost(); } +SerializedScriptValue::SerializedScriptValue(const String& fastPathString) + : m_fastPathString(fastPathString) + , m_isStringFastPath(true) +{ + m_memoryCost = computeMemoryCost(); +} + size_t SerializedScriptValue::computeMemoryCost() const { size_t cost = m_data.size(); @@ -5611,6 +5622,10 @@ size_t SerializedScriptValue::computeMemoryCost() const // for (auto& handle : m_blobHandles) // cost += handle.url().string().sizeInBytes(); + // Account for fast path string memory usage + if (m_isStringFastPath) + cost += m_fastPathString.sizeInBytes(); + return cost; } @@ -5724,7 +5739,7 @@ static bool canDetachRTCDataChannels(const Vector>& channels } #endif -RefPtr SerializedScriptValue::create(JSC::JSGlobalObject& globalObject, JSC::JSValue value, SerializationForStorage forStorage, SerializationErrorMode throwExceptions, SerializationContext serializationContext, SerializationForTransfer forTransfer) +RefPtr SerializedScriptValue::create(JSC::JSGlobalObject& globalObject, JSC::JSValue value, SerializationForStorage forStorage, SerializationErrorMode throwExceptions, SerializationContext serializationContext, SerializationForCrossProcessTransfer forTransfer) { Vector> dummyPorts; auto result = create(globalObject, value, {}, dummyPorts, forStorage, throwExceptions, serializationContext, forTransfer); @@ -5739,15 +5754,31 @@ RefPtr SerializedScriptValue::create(JSC::JSGlobalObject& // return create(globalObject, value, WTFMove(transferList), messagePorts, forStorage, SerializationErrorMode::NonThrowing, serializationContext); // } -ExceptionOr> SerializedScriptValue::create(JSGlobalObject& globalObject, JSValue value, Vector>&& transferList, Vector>& messagePorts, SerializationForStorage forStorage, SerializationContext serializationContext, SerializationForTransfer forTransfer) +ExceptionOr> SerializedScriptValue::create(JSGlobalObject& globalObject, JSValue value, Vector>&& transferList, Vector>& messagePorts, SerializationForStorage forStorage, SerializationContext serializationContext, SerializationForCrossProcessTransfer forTransfer) { return create(globalObject, value, WTFMove(transferList), messagePorts, forStorage, SerializationErrorMode::Throwing, serializationContext, forTransfer); } // ExceptionOr> SerializedScriptValue::create(JSGlobalObject& lexicalGlobalObject, JSValue value, Vector>&& transferList, SerializationForStorage forStorage, SerializationErrorMode throwExceptions, SerializationContext context) -ExceptionOr> SerializedScriptValue::create(JSGlobalObject& lexicalGlobalObject, JSValue value, Vector>&& transferList, Vector>& messagePorts, SerializationForStorage forStorage, SerializationErrorMode throwExceptions, SerializationContext context, SerializationForTransfer forTransfer) +ExceptionOr> SerializedScriptValue::create(JSGlobalObject& lexicalGlobalObject, JSValue value, Vector>&& transferList, Vector>& messagePorts, SerializationForStorage forStorage, SerializationErrorMode throwExceptions, SerializationContext context, SerializationForCrossProcessTransfer forTransfer) { VM& vm = lexicalGlobalObject.vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + // Fast path optimization: for postMessage/structuredClone with pure strings and no transfers + if ((context == SerializationContext::WorkerPostMessage || context == SerializationContext::WindowPostMessage || context == SerializationContext::Default) + && forStorage == SerializationForStorage::No + && forTransfer == SerializationForCrossProcessTransfer::No + && transferList.isEmpty() + && messagePorts.isEmpty() + && value.isString()) { + + JSC::JSString* jsString = asString(value); + String stringValue = jsString->value(&lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, Exception { TypeError }); + return SerializedScriptValue::createStringFastPath(stringValue); + } + Vector> arrayBuffers; // Vector> imageBitmaps; #if ENABLE(OFFSCREEN_CANVAS_IN_WORKERS) @@ -5862,7 +5893,6 @@ ExceptionOr> SerializedScriptValue::create(JSGlobalOb // wasmMemoryHandles, // #endif // blobHandles, buffer, context, *sharedBuffers, forStorage); - auto scope = DECLARE_THROW_SCOPE(vm); auto code = CloneSerializer::serialize(&lexicalGlobalObject, value, messagePorts, arrayBuffers, #if ENABLE(OFFSCREEN_CANVAS_IN_WORKERS) offscreenCanvases, @@ -5965,6 +5995,11 @@ RefPtr SerializedScriptValue::create(StringView string) return adoptRef(*new SerializedScriptValue(WTFMove(buffer))); } +Ref SerializedScriptValue::createStringFastPath(const String& string) +{ + return adoptRef(*new SerializedScriptValue(Bun::toCrossThreadShareable(string))); +} + RefPtr SerializedScriptValue::create(JSContextRef originContext, JSValueRef apiValue, JSValueRef* exception) { JSGlobalObject* lexicalGlobalObject = toJS(originContext); @@ -6082,6 +6117,13 @@ JSValue SerializedScriptValue::deserialize(JSGlobalObject& lexicalGlobalObject, { VM& vm = lexicalGlobalObject.vm(); auto scope = DECLARE_THROW_SCOPE(vm); + // Fast path for string-only values - avoid deserialization overhead + if (m_isStringFastPath) { + if (didFail) + *didFail = false; + return jsString(vm, m_fastPathString); + } + DeserializationResult result = CloneDeserializer::deserialize(&lexicalGlobalObject, globalObject, messagePorts #if ENABLE(OFFSCREEN_CANVAS_IN_WORKERS) , diff --git a/src/bun.js/bindings/webcore/SerializedScriptValue.h b/src/bun.js/bindings/webcore/SerializedScriptValue.h index 11342a26c5..bf2c0c9688 100644 --- a/src/bun.js/bindings/webcore/SerializedScriptValue.h +++ b/src/bun.js/bindings/webcore/SerializedScriptValue.h @@ -75,7 +75,7 @@ enum class SerializationContext { Default, WindowPostMessage }; enum class SerializationForStorage : bool { No, Yes }; -enum class SerializationForTransfer : bool { No, +enum class SerializationForCrossProcessTransfer : bool { No, Yes }; using ArrayBufferContentsArray = Vector; @@ -92,15 +92,18 @@ public: static SYSV_ABI void writeBytesForBun(CloneSerializer*, const uint8_t*, uint32_t); static SYSV_ABI bool isTransferable(JSC::JSGlobalObject* globalObject, JSC::JSValue value); - WEBCORE_EXPORT static ExceptionOr> create(JSC::JSGlobalObject&, JSC::JSValue, Vector>&& transfer, Vector>&, SerializationForStorage = SerializationForStorage::No, SerializationContext = SerializationContext::Default, SerializationForTransfer = SerializationForTransfer::No); + WEBCORE_EXPORT static ExceptionOr> create(JSC::JSGlobalObject&, JSC::JSValue, Vector>&& transfer, Vector>&, SerializationForStorage = SerializationForStorage::No, SerializationContext = SerializationContext::Default, SerializationForCrossProcessTransfer = SerializationForCrossProcessTransfer::No); // WEBCORE_EXPORT static ExceptionOr> create(JSC::JSGlobalObject&, JSC::JSValue, Vector>&& transfer, SerializationForStorage = SerializationForStorage::No, SerializationContext = SerializationContext::Default); - WEBCORE_EXPORT static RefPtr create(JSC::JSGlobalObject&, JSC::JSValue, SerializationForStorage = SerializationForStorage::No, SerializationErrorMode = SerializationErrorMode::Throwing, SerializationContext = SerializationContext::Default, SerializationForTransfer = SerializationForTransfer::No); + WEBCORE_EXPORT static RefPtr create(JSC::JSGlobalObject&, JSC::JSValue, SerializationForStorage = SerializationForStorage::No, SerializationErrorMode = SerializationErrorMode::Throwing, SerializationContext = SerializationContext::Default, SerializationForCrossProcessTransfer = SerializationForCrossProcessTransfer::No); static RefPtr convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value) { return create(globalObject, value, SerializationForStorage::Yes); } WEBCORE_EXPORT static RefPtr create(StringView); + // Fast path for postMessage with pure strings + static Ref createStringFastPath(const String& string); + static Ref nullValue(); WEBCORE_EXPORT JSC::JSValue deserialize(JSC::JSGlobalObject&, JSC::JSGlobalObject*, SerializationErrorMode = SerializationErrorMode::Throwing, bool* didFail = nullptr); @@ -151,7 +154,7 @@ private: // Vector>&& = {}, Vector&& = {} // #endif // ); - static ExceptionOr> create(JSC::JSGlobalObject&, JSC::JSValue, Vector>&& transfer, Vector>&, SerializationForStorage, SerializationErrorMode, SerializationContext, SerializationForTransfer); + static ExceptionOr> create(JSC::JSGlobalObject&, JSC::JSValue, Vector>&& transfer, Vector>&, SerializationForStorage, SerializationErrorMode, SerializationContext, SerializationForCrossProcessTransfer); WEBCORE_EXPORT SerializedScriptValue(Vector&&, std::unique_ptr&& = nullptr #if ENABLE(WEB_RTC) , @@ -200,6 +203,9 @@ private: #endif ); + // Constructor for string fast path + explicit SerializedScriptValue(const String& fastPathString); + size_t computeMemoryCost() const; Vector m_data; @@ -221,6 +227,11 @@ private: Vector m_serializedVideoFrames; #endif // Vector m_blobHandles; + + // Fast path for postMessage with pure strings - avoids serialization overhead + String m_fastPathString; + bool m_isStringFastPath { false }; + size_t m_memoryCost { 0 }; }; diff --git a/src/bun.js/bindings/webcore/StructuredClone.cpp b/src/bun.js/bindings/webcore/StructuredClone.cpp index 143451693d..5c85999cc1 100644 --- a/src/bun.js/bindings/webcore/StructuredClone.cpp +++ b/src/bun.js/bindings/webcore/StructuredClone.cpp @@ -30,6 +30,8 @@ #include "JSDOMBinding.h" #include "JSDOMExceptionHandling.h" #include +#include "SerializedScriptValue.h" +#include "MessagePort.h" namespace WebCore { using namespace JSC; @@ -114,4 +116,115 @@ JSC_DEFINE_HOST_FUNCTION(structuredCloneForStream, (JSGlobalObject * globalObjec return {}; } +JSC_DEFINE_HOST_FUNCTION(jsFunctionStructuredClone, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(globalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + + if (callFrame->argumentCount() == 0) { + throwTypeError(globalObject, throwScope, "structuredClone requires 1 argument"_s); + return {}; + } + + JSC::JSValue value = callFrame->argument(0); + JSC::JSValue options = callFrame->argument(1); + + Vector> transferList; + + if (options.isObject()) { + JSC::JSObject* optionsObject = options.getObject(); + JSC::JSValue transferListValue = optionsObject->get(globalObject, vm.propertyNames->transfer); + RETURN_IF_EXCEPTION(throwScope, {}); + if (transferListValue.isObject()) { + JSC::JSObject* transferListObject = transferListValue.getObject(); + if (auto* transferListArray = jsDynamicCast(transferListObject)) { + for (unsigned i = 0; i < transferListArray->length(); i++) { + JSC::JSValue transferListValue = transferListArray->get(globalObject, i); + RETURN_IF_EXCEPTION(throwScope, {}); + if (transferListValue.isObject()) { + JSC::JSObject* transferListObject = transferListValue.getObject(); + transferList.append(JSC::Strong(vm, transferListObject)); + } + } + } + } + } + + Vector> ports; + ExceptionOr> serialized = SerializedScriptValue::create(*globalObject, value, WTFMove(transferList), ports); + if (serialized.hasException()) { + WebCore::propagateException(*globalObject, throwScope, serialized.releaseException()); + RELEASE_AND_RETURN(throwScope, {}); + } + throwScope.assertNoException(); + + JSValue deserialized = serialized.releaseReturnValue()->deserialize(*globalObject, globalObject, ports); + RETURN_IF_EXCEPTION(throwScope, {}); + + return JSValue::encode(deserialized); +} + +JSC_DEFINE_HOST_FUNCTION(jsFunctionStructuredCloneAdvanced, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(globalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + + if (callFrame->argumentCount() < 4) { + throwTypeError(globalObject, throwScope, "structuredCloneAdvanced requires 3 arguments"_s); + return {}; + } + + JSC::JSValue value = callFrame->argument(0); + JSC::JSValue transferListValue = callFrame->argument(1); + bool isForTransfer = callFrame->argument(2).toBoolean(globalObject); + bool isForStorage = callFrame->argument(3).toBoolean(globalObject); + JSC::JSValue serializationContextValue = callFrame->argument(4); + + SerializationContext serializationContext = SerializationContext::Default; + if (serializationContextValue.isString()) { + if (serializationContextValue.getString(globalObject) == "worker"_s) { + serializationContext = SerializationContext::WorkerPostMessage; + } else if (serializationContextValue.getString(globalObject) == "window"_s) { + serializationContext = SerializationContext::WindowPostMessage; + } else if (serializationContextValue.getString(globalObject) == "postMessage"_s) { + serializationContext = SerializationContext::WindowPostMessage; + } else if (serializationContextValue.getString(globalObject) == "default"_s) { + serializationContext = SerializationContext::Default; + } else { + throwTypeError(globalObject, throwScope, "invalid serialization context"_s); + } + } + + SerializationForCrossProcessTransfer forTransfer = isForTransfer ? SerializationForCrossProcessTransfer::Yes : SerializationForCrossProcessTransfer::No; + SerializationForStorage forStorage = isForStorage ? SerializationForStorage::Yes : SerializationForStorage::No; + + Vector> transferList; + + if (transferListValue.isObject()) { + JSC::JSObject* transferListObject = transferListValue.getObject(); + if (auto* transferListArray = jsDynamicCast(transferListObject)) { + for (unsigned i = 0; i < transferListArray->length(); i++) { + JSC::JSValue transferListValue = transferListArray->get(globalObject, i); + RETURN_IF_EXCEPTION(throwScope, {}); + if (transferListValue.isObject()) { + transferList.append(JSC::Strong(vm, transferListValue.getObject())); + } + } + } + } + + Vector> ports; + ExceptionOr> serialized = SerializedScriptValue::create(*globalObject, value, WTFMove(transferList), ports, forStorage, serializationContext, forTransfer); + if (serialized.hasException()) { + WebCore::propagateException(*globalObject, throwScope, serialized.releaseException()); + RELEASE_AND_RETURN(throwScope, {}); + } + throwScope.assertNoException(); + + JSValue deserialized = serialized.releaseReturnValue()->deserialize(*globalObject, globalObject, ports); + RETURN_IF_EXCEPTION(throwScope, {}); + + return JSValue::encode(deserialized); +} + } // namespace WebCore diff --git a/src/bun.js/bindings/webcore/StructuredClone.h b/src/bun.js/bindings/webcore/StructuredClone.h index af14c08738..380f0523b0 100644 --- a/src/bun.js/bindings/webcore/StructuredClone.h +++ b/src/bun.js/bindings/webcore/StructuredClone.h @@ -36,5 +36,6 @@ namespace WebCore { JSC_DECLARE_HOST_FUNCTION(cloneArrayBuffer); JSC_DECLARE_HOST_FUNCTION(structuredCloneForStream); - +JSC_DECLARE_HOST_FUNCTION(jsFunctionStructuredClone); +JSC_DECLARE_HOST_FUNCTION(jsFunctionStructuredCloneAdvanced); } // namespace WebCore diff --git a/src/bun.js/bindings/webcore/Worker.cpp b/src/bun.js/bindings/webcore/Worker.cpp index 1eb0cec609..3891d6fba3 100644 --- a/src/bun.js/bindings/webcore/Worker.cpp +++ b/src/bun.js/bindings/webcore/Worker.cpp @@ -248,7 +248,7 @@ ExceptionOr Worker::postMessage(JSC::JSGlobalObject& state, JSC::JSValue m MessageWithMessagePorts messageWithMessagePorts { serialized.releaseReturnValue(), disentangledPorts.releaseReturnValue() }; - this->postTaskToWorkerGlobalScope([message = messageWithMessagePorts](auto& context) mutable { + this->postTaskToWorkerGlobalScope([message = WTFMove(messageWithMessagePorts)](auto& context) mutable { Zig::GlobalObject* globalObject = jsCast(context.jsGlobalObject()); auto ports = MessagePort::entanglePorts(context, WTFMove(message.transferredPorts)); diff --git a/src/bun.js/ipc.zig b/src/bun.js/ipc.zig index d9dec7488c..949038c48e 100644 --- a/src/bun.js/ipc.zig +++ b/src/bun.js/ipc.zig @@ -117,7 +117,12 @@ const advanced = struct { } pub fn serialize(writer: *bun.io.StreamBuffer, global: *jsc.JSGlobalObject, value: JSValue, is_internal: IsInternal) !usize { - const serialized = try value.serialize(global, true); + const serialized = try value.serialize(global, .{ + // IPC sends across process. + .forCrossProcessTransfer = true, + + .forStorage = false, + }); defer serialized.deinit(); const size: u32 = @intCast(serialized.data.len); @@ -510,7 +515,7 @@ pub const SendQueue = struct { } this.keep_alive.disable(); this.socket = .closed; - this._onAfterIPCClosed(); + this.getGlobalThis().bunVM().enqueueTask(jsc.ManagedTask.New(SendQueue, _onAfterIPCClosed).init(this)); } fn _windowsClose(this: *SendQueue) void { log("SendQueue#_windowsClose", .{}); @@ -519,7 +524,7 @@ pub const SendQueue = struct { pipe.data = pipe; pipe.close(&_windowsOnClosed); this._socketClosed(); - this._onAfterIPCClosed(); + this.getGlobalThis().bunVM().enqueueTask(jsc.ManagedTask.New(SendQueue, _onAfterIPCClosed).init(this)); } fn _windowsOnClosed(windows: *uv.Pipe) callconv(.C) void { log("SendQueue#_windowsOnClosed", .{}); @@ -1075,8 +1080,7 @@ fn handleIPCMessage(send_queue: *SendQueue, message: DecodedIPCMessage, globalTh if (!ack) return; // Get file descriptor and clear it - const fd = send_queue.incoming_fd.?; - send_queue.incoming_fd = null; + const fd: bun.FD = bun.take(&send_queue.incoming_fd).?; const target: bun.jsc.JSValue = switch (send_queue.owner) { .subprocess => |subprocess| subprocess.this_jsvalue, @@ -1353,7 +1357,7 @@ pub const IPCHandlers = struct { pub fn onClose(send_queue: *SendQueue) void { log("NewNamedPipeIPCHandler#onClose\n", .{}); - send_queue._onAfterIPCClosed(); + send_queue.getGlobalThis().bunVM().enqueueTask(jsc.ManagedTask.New(SendQueue, SendQueue._onAfterIPCClosed).init(send_queue)); } }; }; diff --git a/src/bun.js/modules/BunJSCModule.h b/src/bun.js/modules/BunJSCModule.h index 326a944945..5d511fb498 100644 --- a/src/bun.js/modules/BunJSCModule.h +++ b/src/bun.js/modules/BunJSCModule.h @@ -777,7 +777,7 @@ JSC_DEFINE_HOST_FUNCTION(functionSerialize, Vector> transferList; Vector> dummyPorts; - ExceptionOr> serialized = SerializedScriptValue::create(*globalObject, value, WTFMove(transferList), dummyPorts); + ExceptionOr> serialized = SerializedScriptValue::create(*globalObject, value, WTFMove(transferList), dummyPorts, SerializationForStorage::Yes); EXCEPTION_ASSERT(serialized.hasException() == !!throwScope.exception()); if (serialized.hasException()) { WebCore::propagateException(*globalObject, throwScope, serialized.releaseException()); diff --git a/src/bun.js/node/net/BlockList.zig b/src/bun.js/node/net/BlockList.zig index 38ddfad2e5..1a1ed80a19 100644 --- a/src/bun.js/node/net/BlockList.zig +++ b/src/bun.js/node/net/BlockList.zig @@ -11,6 +11,9 @@ globalThis: *jsc.JSGlobalObject, da_rules: std.ArrayList(Rule), mutex: bun.Mutex = .{}, +/// We cannot lock/unlock a mutex +estimated_size: std.atomic.Value(u32) = .init(0), + pub fn constructor(globalThis: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame) bun.JSError!*@This() { _ = callFrame; const ptr = @This().new(.{ @@ -20,10 +23,9 @@ pub fn constructor(globalThis: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame) b return ptr; } +/// May be called from any thread. pub fn estimatedSize(this: *@This()) usize { - this.mutex.lock(); - defer this.mutex.unlock(); - return @sizeOf(@This()) + (@sizeOf(Rule) * this.da_rules.items.len); + return (@sizeOf(@This()) + this.estimated_size.load(.seq_cst)) / this.ref_count.get(); } pub fn finalize(this: *@This()) void { @@ -42,8 +44,6 @@ pub fn isBlockList(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) b } pub fn addAddress(this: *@This(), globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue { - this.mutex.lock(); - defer this.mutex.unlock(); const arguments = callframe.argumentsAsArray(2); const address_js, var family_js = arguments; if (family_js.isUndefined()) family_js = bun.String.static("ipv4").toJS(globalThis); @@ -52,13 +52,15 @@ pub fn addAddress(this: *@This(), globalThis: *jsc.JSGlobalObject, callframe: *j try validators.validateString(globalThis, family_js, "family", .{}); break :blk (try SocketAddress.initFromAddrFamily(globalThis, address_js, family_js))._addr; }; + + this.mutex.lock(); + defer this.mutex.unlock(); try this.da_rules.insert(0, .{ .addr = address }); + _ = this.estimated_size.fetchAdd(@sizeOf(Rule), .monotonic); return .js_undefined; } pub fn addRange(this: *@This(), globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue { - this.mutex.lock(); - defer this.mutex.unlock(); const arguments = callframe.argumentsAsArray(3); const start_js, const end_js, var family_js = arguments; if (family_js.isUndefined()) family_js = bun.String.static("ipv4").toJS(globalThis); @@ -72,18 +74,19 @@ pub fn addRange(this: *@This(), globalThis: *jsc.JSGlobalObject, callframe: *jsc try validators.validateString(globalThis, family_js, "family", .{}); break :blk (try SocketAddress.initFromAddrFamily(globalThis, end_js, family_js))._addr; }; - if (_compare(start, end)) |ord| { + if (_compare(&start, &end)) |ord| { if (ord.compare(.gt)) { return globalThis.throwInvalidArgumentValueCustom("start", start_js, "must come before end"); } } + this.mutex.lock(); + defer this.mutex.unlock(); try this.da_rules.insert(0, .{ .range = .{ .start = start, .end = end } }); + _ = this.estimated_size.fetchAdd(@sizeOf(Rule), .monotonic); return .js_undefined; } pub fn addSubnet(this: *@This(), globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue { - this.mutex.lock(); - defer this.mutex.unlock(); const arguments = callframe.argumentsAsArray(3); const network_js, const prefix_js, var family_js = arguments; if (family_js.isUndefined()) family_js = bun.String.static("ipv4").toJS(globalThis); @@ -98,17 +101,18 @@ pub fn addSubnet(this: *@This(), globalThis: *jsc.JSGlobalObject, callframe: *js std.posix.AF.INET6 => prefix = @intCast(try validators.validateInt32(globalThis, prefix_js, "prefix", .{}, 0, 128)), else => {}, } + this.mutex.lock(); + defer this.mutex.unlock(); try this.da_rules.insert(0, .{ .subnet = .{ .network = network, .prefix = prefix } }); + _ = this.estimated_size.fetchAdd(@sizeOf(Rule), .monotonic); return .js_undefined; } pub fn check(this: *@This(), globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue { - this.mutex.lock(); - defer this.mutex.unlock(); const arguments = callframe.argumentsAsArray(2); const address_js, var family_js = arguments; if (family_js.isUndefined()) family_js = bun.String.static("ipv4").toJS(globalThis); - const address = if (address_js.as(SocketAddress)) |sa| sa._addr else blk: { + const address = &(if (address_js.as(SocketAddress)) |sa| sa._addr else blk: { try validators.validateString(globalThis, address_js, "address", .{}); try validators.validateString(globalThis, family_js, "family", .{}); break :blk (SocketAddress.initFromAddrFamily(globalThis, address_js, family_js) catch |err| { @@ -116,19 +120,21 @@ pub fn check(this: *@This(), globalThis: *jsc.JSGlobalObject, callframe: *jsc.Ca globalThis.clearException(); return .jsBoolean(false); })._addr; - }; - for (this.da_rules.items) |item| { - switch (item) { - .addr => |a| { + }); + this.mutex.lock(); + defer this.mutex.unlock(); + for (this.da_rules.items) |*item| { + switch (item.*) { + .addr => |*a| { const order = _compare(address, a) orelse continue; if (order.compare(.eq)) return .jsBoolean(true); }, - .range => |r| { - const os = _compare(address, r.start) orelse continue; - const oe = _compare(address, r.end) orelse continue; + .range => |*r| { + const os = _compare(address, &r.start) orelse continue; + const oe = _compare(address, &r.end) orelse continue; if (os.compare(.gte) and oe.compare(.lte)) return .jsBoolean(true); }, - .subnet => |s| { + .subnet => |*s| { if (address.as_v4()) |ip_addr| if (s.network.as_v4()) |subnet_addr| { if (s.prefix == 32) if (ip_addr == subnet_addr) (return .jsBoolean(true)) else continue; const one: u32 = 1; @@ -154,28 +160,30 @@ pub fn check(this: *@This(), globalThis: *jsc.JSGlobalObject, callframe: *jsc.Ca } pub fn rules(this: *@This(), globalThis: *jsc.JSGlobalObject) bun.JSError!jsc.JSValue { + + // GC must be able to visit + var array = try jsc.JSArray.createEmpty(globalThis, 0); + this.mutex.lock(); defer this.mutex.unlock(); - var list = std.ArrayList(jsc.JSValue).initCapacity(bun.default_allocator, this.da_rules.items.len) catch bun.outOfMemory(); - defer list.deinit(); - for (this.da_rules.items) |rule| { - switch (rule) { - .addr => |a| { + for (this.da_rules.items) |*rule| { + switch (rule.*) { + .addr => |*a| { var buf: [SocketAddress.inet.INET6_ADDRSTRLEN]u8 = @splat(0); - list.appendAssumeCapacity(try bun.String.createFormatForJS(globalThis, "Address: {s} {s}", .{ a.family().upper(), a.fmt(&buf) })); + try array.push(globalThis, try bun.String.createFormatForJS(globalThis, "Address: {s} {s}", .{ a.family().upper(), a.fmt(&buf) })); }, - .range => |r| { + .range => |*r| { var buf_s: [SocketAddress.inet.INET6_ADDRSTRLEN]u8 = @splat(0); var buf_e: [SocketAddress.inet.INET6_ADDRSTRLEN]u8 = @splat(0); - list.appendAssumeCapacity(try bun.String.createFormatForJS(globalThis, "Range: {s} {s}-{s}", .{ r.start.family().upper(), r.start.fmt(&buf_s), r.end.fmt(&buf_e) })); + try array.push(globalThis, try bun.String.createFormatForJS(globalThis, "Range: {s} {s}-{s}", .{ r.start.family().upper(), r.start.fmt(&buf_s), r.end.fmt(&buf_e) })); }, - .subnet => |s| { + .subnet => |*s| { var buf: [SocketAddress.inet.INET6_ADDRSTRLEN]u8 = @splat(0); - list.appendAssumeCapacity(try bun.String.createFormatForJS(globalThis, "Subnet: {s} {s}/{d}", .{ s.network.family().upper(), s.network.fmt(&buf), s.prefix })); + try array.push(globalThis, try bun.String.createFormatForJS(globalThis, "Subnet: {s} {s}/{d}", .{ s.network.family().upper(), s.network.fmt(&buf), s.prefix })); }, } } - return jsc.JSArray.create(globalThis, list.items); + return array; } pub fn onStructuredCloneSerialize(this: *@This(), globalThis: *jsc.JSGlobalObject, ctx: *anyopaque, writeBytes: *const fn (*anyopaque, ptr: [*]const u8, len: u32) callconv(jsc.conv) void) void { @@ -216,13 +224,13 @@ pub const Rule = union(enum) { subnet: struct { network: sockaddr, prefix: u8 }, }; -fn _compare(l: sockaddr, r: sockaddr) ?std.math.Order { +fn _compare(l: *const sockaddr, r: *const sockaddr) ?std.math.Order { if (l.as_v4()) |l_4| if (r.as_v4()) |r_4| return std.math.order(@byteSwap((l_4)), @byteSwap((r_4))); - if (l.sin.family == std.posix.AF.INET6 and r.sin.family == std.posix.AF.INET6) return _compare_ipv6(l.sin6, r.sin6); + if (l.sin.family == std.posix.AF.INET6 and r.sin.family == std.posix.AF.INET6) return _compare_ipv6(&l.sin6, &r.sin6); return null; } -fn _compare_ipv6(l: sockaddr.in6, r: sockaddr.in6) std.math.Order { +fn _compare_ipv6(l: *const sockaddr.in6, r: *const sockaddr.in6) std.math.Order { return std.math.order(@byteSwap((@as(u128, @bitCast(l.addr)))), @byteSwap((@as(u128, @bitCast(r.addr))))); } diff --git a/src/bun.js/webcore/Blob.zig b/src/bun.js/webcore/Blob.zig index 363be1758f..dc48820109 100644 --- a/src/bun.js/webcore/Blob.zig +++ b/src/bun.js/webcore/Blob.zig @@ -2707,7 +2707,7 @@ pub fn getWriter( .path = ZigString.Slice.fromUTF8NeverFree( store.data.file.pathlike.path.slice(), ).cloneIfNeeded( - globalThis.allocator(), + bun.default_allocator, ) catch bun.outOfMemory(), }; } @@ -4390,7 +4390,7 @@ pub const Internal = struct { pub fn toStringOwned(this: *@This(), globalThis: *jsc.JSGlobalObject) JSValue { const bytes_without_bom = strings.withoutUTF8BOM(this.bytes.items); - if (strings.toUTF16Alloc(globalThis.allocator(), bytes_without_bom, false, false) catch &[_]u16{}) |out| { + if (strings.toUTF16Alloc(bun.default_allocator, bytes_without_bom, false, false) catch &[_]u16{}) |out| { const return_value = ZigString.toExternalU16(out.ptr, out.len, globalThis); return_value.ensureStillAlive(); this.deinit(); diff --git a/src/bun.js/webcore/Request.zig b/src/bun.js/webcore/Request.zig index 205a2d90b9..26e75c05ed 100644 --- a/src/bun.js/webcore/Request.zig +++ b/src/bun.js/webcore/Request.zig @@ -572,7 +572,7 @@ pub fn constructInto(globalThis: *jsc.JSGlobalObject, arguments: []const jsc.JSV if (value_type == .DOMWrapper) { if (value.asDirect(Request)) |request| { if (values_to_try.len == 1) { - try request.cloneInto(&req, globalThis.allocator(), globalThis, fields.contains(.url)); + try request.cloneInto(&req, bun.default_allocator, globalThis, fields.contains(.url)); success = true; return req; } diff --git a/src/bun.js/webcore/TextDecoder.zig b/src/bun.js/webcore/TextDecoder.zig index d21cf99d2d..397d15a7b9 100644 --- a/src/bun.js/webcore/TextDecoder.zig +++ b/src/bun.js/webcore/TextDecoder.zig @@ -203,7 +203,7 @@ fn decodeSlice(this: *TextDecoder, globalThis: *jsc.JSGlobalObject, buffer_slice // // It's not clear why we couldn't jusst use Latin1 here, but tests failures proved it necessary. const out_length = strings.elementLengthLatin1IntoUTF16([]const u8, buffer_slice); - const bytes = try globalThis.allocator().alloc(u16, out_length); + const bytes = try bun.default_allocator.alloc(u16, out_length); const out = strings.copyLatin1IntoUTF16([]u16, bytes, []const u8, buffer_slice); return ZigString.toExternalU16(bytes.ptr, out.written, globalThis); diff --git a/src/bun.js/webcore/response.classes.ts b/src/bun.js/webcore/response.classes.ts index 06cc53375d..faa6053fe9 100644 --- a/src/bun.js/webcore/response.classes.ts +++ b/src/bun.js/webcore/response.classes.ts @@ -133,7 +133,14 @@ export default [ JSType: "0b11101110", klass: {}, configurable: false, - structuredClone: { transferable: false, tag: 254 }, + structuredClone: { + transferable: false, + tag: 254, + + // TODO: fix this. + // We should support it unless it's a file descriptor. + storable: true, + }, estimatedSize: true, values: ["stream"], overridesToJS: true, diff --git a/src/codegen/bundle-modules.ts b/src/codegen/bundle-modules.ts index bec07d0e16..a7645c1625 100644 --- a/src/codegen/bundle-modules.ts +++ b/src/codegen/bundle-modules.ts @@ -42,7 +42,7 @@ const JS_DIR = path.join(CMAKE_BUILD_ROOT, "js"); const t = new Bun.Transpiler({ loader: "tsx" }); let start = performance.now(); -const silent = process.env.BUN_SILENT === "1"; +const silent = process.env.BUN_SILENT === "1" || process.env.CLAUDECODE; function markVerbose(log: string) { const now = performance.now(); console.log(`${log} (${(now - start).toFixed(0)}ms)`); diff --git a/src/codegen/class-definitions.ts b/src/codegen/class-definitions.ts index ab123f9d5f..6c13fc37d2 100644 --- a/src/codegen/class-definitions.ts +++ b/src/codegen/class-definitions.ts @@ -212,7 +212,7 @@ export class ClassDefinition { configurable?: boolean; enumerable?: boolean; - structuredClone?: { transferable: boolean; tag: number }; + structuredClone?: { transferable: boolean; tag: number; storable: boolean }; inspectCustom?: boolean; callbacks?: Record; diff --git a/src/codegen/cppbind.ts b/src/codegen/cppbind.ts index 46f17a46f7..9067b0932f 100644 --- a/src/codegen/cppbind.ts +++ b/src/codegen/cppbind.ts @@ -798,21 +798,22 @@ async function main() { const resultSourceLinksContents = resultSourceLinks.join("\n"); if ((await readFileOrEmpty(resultSourceLinksFilePath)) !== resultSourceLinksContents) { await Bun.write(resultSourceLinksFilePath, resultSourceLinksContents); + const now = Date.now(); + const sin = Math.round(((Math.sin((now / 1000) * 1) + 1) / 2) * 0); + if (process.env.CI) { + console.log( + " ".repeat(sin) + + (errors.length > 0 ? "✗" : "✓") + + " cppbind.ts generated bindings to " + + resultFilePath + + (errors.length > 0 ? " with errors" : "") + + " in " + + (now - start) + + "ms", + ); + } } - const now = Date.now(); - const sin = Math.round(((Math.sin((now / 1000) * 1) + 1) / 2) * 0); - - console.log( - " ".repeat(sin) + - (errors.length > 0 ? "✗" : "✓") + - " cppbind.ts generated bindings to " + - resultFilePath + - (errors.length > 0 ? " with errors" : "") + - " in " + - (now - start) + - "ms", - ); if (errors.length > 0) { process.exit(1); } diff --git a/src/codegen/generate-classes.ts b/src/codegen/generate-classes.ts index ba4039e2fb..2f1257c661 100644 --- a/src/codegen/generate-classes.ts +++ b/src/codegen/generate-classes.ts @@ -2567,10 +2567,13 @@ class StructuredCloneableSerialize { CppStructuredCloneableSerializeFunction cppWriteBytes; ZigStructuredCloneableSerializeFunction zigFunction; - uint8_t tag; + uint8_t tag = 0; // the type from zig - void* impl; + void* impl = nullptr; + + bool isForTransfer = false; + bool isForStorage = false; static std::optional fromJS(JSC::JSValue); void write(CloneSerializer* serializer, JSC::JSGlobalObject* globalObject) @@ -2601,7 +2604,7 @@ function writeCppSerializers() { return StructuredCloneableSerialize { .cppWriteBytes = SerializedScriptValue::writeBytesForBun, .zigFunction = ${symbolName( klass.name, "onStructuredCloneSerialize", - )}, .tag = ${klass.structuredClone.tag}, .impl = result->wrapped() }; + )}, .tag = ${klass.structuredClone.tag}, .impl = result->wrapped(), .isForTransfer = ${!!klass.structuredClone.transferable}, .isForStorage = ${!!klass.structuredClone.storable} }; } `; } diff --git a/src/js/internal-for-testing.ts b/src/js/internal-for-testing.ts index 77951d03da..76e9c391d7 100644 --- a/src/js/internal-for-testing.ts +++ b/src/js/internal-for-testing.ts @@ -193,3 +193,11 @@ interface setSocketOptionsFn { } export const setSocketOptions: setSocketOptionsFn = $newZigFunction("socket.zig", "jsSetSocketOptions", 3); +type SerializationContext = "worker" | "window" | "postMessage" | "default"; +export const structuredCloneAdvanced: ( + value: any, + transferList: any[], + forTransfer: boolean, + forStorage: boolean, + serializationContext: SerializationContext, +) => any = $newCppFunction("StructuredClone.cpp", "jsFunctionStructuredCloneAdvanced", 5); diff --git a/test/harness.ts b/test/harness.ts index c2e49d866a..2b4d9a988c 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -269,7 +269,7 @@ export function tempDirWithFilesAnon(filesOrAbsolutePathToCopyFolderFrom: Direct return base; } -export function bunRun(file: string, env?: Record | NodeJS.ProcessEnv) { +export function bunRun(file: string, env?: Record | NodeJS.ProcessEnv, dump = false) { var path = require("path"); const result = Bun.spawnSync([bunExe(), file], { cwd: path.dirname(file), @@ -278,11 +278,21 @@ export function bunRun(file: string, env?: Record | NodeJS.Proce NODE_ENV: undefined, ...env, }, + stdin: "ignore", + stdout: !dump ? "pipe" : "inherit", + stderr: !dump ? "pipe" : "inherit", }); - if (!result.success) throw new Error(result.stderr.toString("utf8")); + if (!result.success) { + if (dump) { + throw new Error( + "exited with code " + result.exitCode + (result.signalCode ? `signal: ${result.signalCode}` : ""), + ); + } + throw new Error(String(result.stderr) + "\n" + String(result.stdout)); + } return { - stdout: result.stdout.toString("utf8").trim(), - stderr: result.stderr.toString("utf8").trim(), + stdout: String(result.stdout ?? "").trim(), + stderr: String(result.stderr ?? "").trim(), }; } diff --git a/test/js/bun/jsc/string-noAtomize.test.ts b/test/js/bun/jsc/string-noAtomize.test.ts new file mode 100644 index 0000000000..7eb41fb5ce --- /dev/null +++ b/test/js/bun/jsc/string-noAtomize.test.ts @@ -0,0 +1,27 @@ +// @bun +const { describe, expect, it, test } = Bun.jest(import.meta.path); + +test("string no atomize should work", () => { + var str = "hello"; + if (structuredClone(str) !== str) { + throw new Error("FAIL"); + } + + var obj = {}; + for (var i = 0; i < 10000; i++) { + obj[str] = str; + } + + if (Object.getOwnPropertyNames(obj).length !== 1) { + throw new Error("FAIL"); + } + + var obj2 = {}; + for (var i = 0; i < 10000; i++) { + obj2[str] = Object.getOwnPropertyNames(obj)[0]; + } + + if (structuredClone(Object.getOwnPropertyNames(obj2)[0]) !== str) { + throw new Error("FAIL"); + } +}); diff --git a/test/js/node/cluster.test.ts b/test/js/node/cluster.test.ts index 2e2792042d..a6f4bb02ea 100644 --- a/test/js/node/cluster.test.ts +++ b/test/js/node/cluster.test.ts @@ -30,7 +30,7 @@ if (cluster.isPrimary) { } `, }); - bunRun(joinP(dir, "index.ts"), bunEnv); + bunRun(joinP(dir, "index.ts"), bunEnv, true); }); test("cloneable and non-transferable not-equals (BunFile)", () => { @@ -49,6 +49,11 @@ if (cluster.isPrimary) { worker.on("online", function () { worker.send({ file }); }); + worker.on("exit", function (code, signal) { + if (code !== 0) { + process.exit(code); + } + }); worker.on("message", function (data) { worker.kill(); const { file } = data; @@ -63,10 +68,14 @@ if (cluster.isPrimary) { console.log("W", msg); process.send!(msg); }); + process.on("uncaughtExceptionMonitor", (error) => { + console.error(error); + process.exit(1); + }); } `, }); - bunRun(joinP(dir, "index.ts"), bunEnv); + bunRun(joinP(dir, "index.ts"), bunEnv, true); }); test("cloneable and non-transferable not-equals (net.BlockList)", () => { @@ -84,6 +93,11 @@ if (cluster.isPrimary) { worker.on("online", function () { worker.send({ blocklist }); }); + worker.on("exit", function (code, signal) { + if (code !== 0) { + process.exit(code); + } + }); worker.on("message", function (data) { worker.kill(); const { blocklist } = data; @@ -95,10 +109,14 @@ if (cluster.isPrimary) { } else { process.on("message", msg => { console.log("W", msg); - process.send!(msg); + process.send!(msg); + }); + process.on("uncaughtExceptionMonitor", (error) => { + console.error(error); + process.exit(1); }); } `, }); - bunRun(joinP(dir, "index.ts"), bunEnv); + bunRun(joinP(dir, "index.ts"), bunEnv, true); }); diff --git a/test/js/web/structured-clone-fastpath.test.ts b/test/js/web/structured-clone-fastpath.test.ts new file mode 100644 index 0000000000..147fd221b6 --- /dev/null +++ b/test/js/web/structured-clone-fastpath.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, test } from "bun:test"; + +describe("Structured Clone Fast Path", () => { + test("structuredClone should use a constant amount of memory for string inputs", () => { + // Create a 100KB string to test fast path + const largeString = Buffer.alloc(512 * 1024).toString(); + for (let i = 0; i < 100; i++) { + structuredClone(largeString); + } + const rss = process.memoryUsage.rss(); + for (let i = 0; i < 10000; i++) { + structuredClone(largeString); + } + const rss2 = process.memoryUsage.rss(); + const delta = rss2 - rss; + expect(delta).toBeLessThan(1024 * 1024); + }); +}); diff --git a/test/js/web/workers/structured-clone.test.ts b/test/js/web/workers/structured-clone.test.ts index 0ce2ccc833..c39f648533 100644 --- a/test/js/web/workers/structured-clone.test.ts +++ b/test/js/web/workers/structured-clone.test.ts @@ -1,246 +1,282 @@ +import { deserialize, serialize } from "bun:jsc"; import { openSync } from "fs"; +import { bunEnv } from "harness"; +import { bunExe } from "js/bun/shell/test_builder"; import { join } from "path"; +function jscSerializeRoundtrip(value: any) { + const serialized = serialize(value); + const cloned = deserialize(serialized); + return cloned; +} -describe("structured clone", () => { - let primitives_tests = [ - { description: "primitive undefined", value: undefined }, - { description: "primitive null", value: null }, - { description: "primitive true", value: true }, - { description: "primitive false", value: false }, - { description: "primitive string, empty string", value: "" }, - { description: "primitive string, lone high surrogate", value: "\uD800" }, - { description: "primitive string, lone low surrogate", value: "\uDC00" }, - { description: "primitive string, NUL", value: "\u0000" }, - { description: "primitive string, astral character", value: "\uDBFF\uDFFD" }, - { description: "primitive number, 0.2", value: 0.2 }, - { description: "primitive number, 0", value: 0 }, - { description: "primitive number, -0", value: -0 }, - { description: "primitive number, NaN", value: NaN }, - { description: "primitive number, Infinity", value: Infinity }, - { description: "primitive number, -Infinity", value: -Infinity }, - { description: "primitive number, 9007199254740992", value: 9007199254740992 }, - { description: "primitive number, -9007199254740992", value: -9007199254740992 }, - { description: "primitive number, 9007199254740994", value: 9007199254740994 }, - { description: "primitive number, -9007199254740994", value: -9007199254740994 }, - { description: "primitive BigInt, 0n", value: 0n }, - { description: "primitive BigInt, -0n", value: -0n }, - { description: "primitive BigInt, -9007199254740994000n", value: -9007199254740994000n }, - { - description: "primitive BigInt, -9007199254740994000900719925474099400090071992547409940009007199254740994000n", - value: -9007199254740994000900719925474099400090071992547409940009007199254740994000n, - }, - ]; - for (let { description, value } of primitives_tests) { - test(description, () => { - const cloned = structuredClone(value); - expect(cloned).toBe(value); - }); - } +function jscSerializeRoundtripCrossProcess(original: any) { + const serialized = serialize(original); - test("Array with primitives", () => { - const input = [ - undefined, - null, - true, - false, - "", - "\uD800", - "\uDC00", - "\u0000", - "\uDBFF\uDFFD", - 0.2, - 0, - -0, - NaN, - Infinity, - -Infinity, - 9007199254740992, - -9007199254740992, - 9007199254740994, - -9007199254740994, - -12n, - -0n, - 0n, + const result = Bun.spawnSync({ + cmd: [ + bunExe(), + "-e", + ` + import {deserialize, serialize} from "bun:jsc"; + const serialized = deserialize(await Bun.stdin.bytes()); + const cloned = serialize(serialized); + process.stdout.write(cloned); + `, + ], + env: bunEnv, + stdin: serialized, + stdout: "pipe", + stderr: "inherit", + }); + return deserialize(result.stdout); +} + +for (const structuredCloneFn of [structuredClone, jscSerializeRoundtrip, jscSerializeRoundtripCrossProcess]) { + describe(structuredCloneFn.name, () => { + let primitives_tests = [ + { description: "primitive undefined", value: undefined }, + { description: "primitive null", value: null }, + { description: "primitive true", value: true }, + { description: "primitive false", value: false }, + { description: "primitive string, empty string", value: "" }, + { description: "primitive string, lone high surrogate", value: "\uD800" }, + { description: "primitive string, lone low surrogate", value: "\uDC00" }, + { description: "primitive string, NUL", value: "\u0000" }, + { description: "primitive string, astral character", value: "\uDBFF\uDFFD" }, + { description: "primitive number, 0.2", value: 0.2 }, + { description: "primitive number, 0", value: 0 }, + { description: "primitive number, -0", value: -0 }, + { description: "primitive number, NaN", value: NaN }, + { description: "primitive number, Infinity", value: Infinity }, + { description: "primitive number, -Infinity", value: -Infinity }, + { description: "primitive number, 9007199254740992", value: 9007199254740992 }, + { description: "primitive number, -9007199254740992", value: -9007199254740992 }, + { description: "primitive number, 9007199254740994", value: 9007199254740994 }, + { description: "primitive number, -9007199254740994", value: -9007199254740994 }, + { description: "primitive BigInt, 0n", value: 0n }, + { description: "primitive BigInt, -0n", value: -0n }, + { description: "primitive BigInt, -9007199254740994000n", value: -9007199254740994000n }, + { + description: "primitive BigInt, -9007199254740994000900719925474099400090071992547409940009007199254740994000n", + value: -9007199254740994000900719925474099400090071992547409940009007199254740994000n, + }, ]; - const cloned = structuredClone(input); - expect(cloned).toBeInstanceOf(Array); - expect(cloned).not.toBe(input); - expect(cloned.length).toEqual(input.length); - for (const x in input) { - expect(cloned[x]).toBe(input[x]); - } - }); - test("Object with primitives", () => { - const input: any = { - undefined: undefined, - null: null, - true: true, - false: false, - empty: "", - "high surrogate": "\uD800", - "low surrogate": "\uDC00", - nul: "\u0000", - astral: "\uDBFF\uDFFD", - "0.2": 0.2, - "0": 0, - "-0": -0, - NaN: NaN, - Infinity: Infinity, - "-Infinity": -Infinity, - "9007199254740992": 9007199254740992, - "-9007199254740992": -9007199254740992, - "9007199254740994": 9007199254740994, - "-9007199254740994": -9007199254740994, - "-12n": -12n, - "-0n": -0n, - "0n": 0n, - }; - const cloned = structuredClone(input); - expect(cloned).toBeInstanceOf(Object); - expect(cloned).not.toBeInstanceOf(Array); - expect(cloned).not.toBe(input); - for (const x in input) { - expect(cloned[x]).toBe(input[x]); - } - }); - - test("map", () => { - const input = new Map(); - input.set("a", 1); - input.set("b", 2); - input.set("c", 3); - const cloned = structuredClone(input); - expect(cloned).toBeInstanceOf(Map); - expect(cloned).not.toBe(input); - expect(cloned.size).toEqual(input.size); - for (const [key, value] of input) { - expect(cloned.get(key)).toBe(value); - } - }); - - test("set", () => { - const input = new Set(); - input.add("a"); - input.add("b"); - input.add("c"); - const cloned = structuredClone(input); - expect(cloned).toBeInstanceOf(Set); - expect(cloned).not.toBe(input); - expect(cloned.size).toEqual(input.size); - for (const value of input) { - expect(cloned.has(value)).toBe(true); - } - }); - - describe("bun blobs work", () => { - test("simple", async () => { - const blob = new Blob(["hello"], { type: "application/octet-stream" }); - const cloned = structuredClone(blob); - await compareBlobs(blob, cloned); - }); - test("empty", async () => { - const emptyBlob = new Blob([], { type: "" }); - const clonedEmpty = structuredClone(emptyBlob); - await compareBlobs(emptyBlob, clonedEmpty); - }); - test("empty with type", async () => { - const emptyBlob = new Blob([], { type: "application/octet-stream" }); - const clonedEmpty = structuredClone(emptyBlob); - await compareBlobs(emptyBlob, clonedEmpty); - }); - test("unknown type", async () => { - const blob = new Blob(["hello type"], { type: "this is type" }); - const cloned = structuredClone(blob); - await compareBlobs(blob, cloned); - }); - test("file from path", async () => { - const blob = Bun.file(join(import.meta.dir, "example.txt")); - const cloned = structuredClone(blob); - expect(cloned.lastModified).toBe(blob.lastModified); - expect(cloned.name).toBe(blob.name); - }); - test("file from fd", async () => { - const fd = openSync(join(import.meta.dir, "example.txt"), "r"); - const blob = Bun.file(fd); - const cloned = structuredClone(blob); - expect(cloned.lastModified).toBe(blob.lastModified); - expect(cloned.name).toBe(blob.name); - }); - describe("dom file", async () => { - test("without lastModified", async () => { - const file = new File(["hi"], "example.txt", { type: "text/plain" }); - expect(file.lastModified).toBeGreaterThan(0); - expect(file.name).toBe("example.txt"); - expect(file.size).toBe(2); - const cloned = structuredClone(file); - expect(cloned.lastModified).toBe(file.lastModified); - expect(cloned.name).toBe(file.name); - expect(cloned.size).toBe(file.size); + for (let { description, value } of primitives_tests) { + test(description, () => { + const cloned = structuredCloneFn(value); + expect(cloned).toBe(value); }); - test("with lastModified", async () => { - const file = new File(["hi"], "example.txt", { type: "text/plain", lastModified: 123 }); - expect(file.lastModified).toBe(123); - expect(file.name).toBe("example.txt"); - expect(file.size).toBe(2); - const cloned = structuredClone(file); - expect(cloned.lastModified).toBe(123); - expect(cloned.name).toBe(file.name); - expect(cloned.size).toBe(file.size); + } + + test("Array with primitives", () => { + const input = [ + undefined, + null, + true, + false, + "", + "\uD800", + "\uDC00", + "\u0000", + "\uDBFF\uDFFD", + 0.2, + 0, + -0, + NaN, + Infinity, + -Infinity, + 9007199254740992, + -9007199254740992, + 9007199254740994, + -9007199254740994, + -12n, + -0n, + 0n, + ]; + const cloned = structuredCloneFn(input); + expect(cloned).toBeInstanceOf(Array); + expect(cloned).not.toBe(input); + expect(cloned.length).toEqual(input.length); + for (const x in input) { + expect(cloned[x]).toBe(input[x]); + } + }); + test("Object with primitives", () => { + const input: any = { + undefined: undefined, + null: null, + true: true, + false: false, + empty: "", + "high surrogate": "\uD800", + "low surrogate": "\uDC00", + nul: "\u0000", + astral: "\uDBFF\uDFFD", + "0.2": 0.2, + "0": 0, + "-0": -0, + NaN: NaN, + Infinity: Infinity, + "-Infinity": -Infinity, + "9007199254740992": 9007199254740992, + "-9007199254740992": -9007199254740992, + "9007199254740994": 9007199254740994, + "-9007199254740994": -9007199254740994, + "-12n": -12n, + "-0n": -0n, + "0n": 0n, + }; + const cloned = structuredCloneFn(input); + expect(cloned).toBeInstanceOf(Object); + expect(cloned).not.toBeInstanceOf(Array); + expect(cloned).not.toBe(input); + for (const x in input) { + expect(cloned[x]).toBe(input[x]); + } + }); + + test("map", () => { + const input = new Map(); + input.set("a", 1); + input.set("b", 2); + input.set("c", 3); + const cloned = structuredCloneFn(input); + expect(cloned).toBeInstanceOf(Map); + expect(cloned).not.toBe(input); + expect(cloned.size).toEqual(input.size); + for (const [key, value] of input) { + expect(cloned.get(key)).toBe(value); + } + }); + + test("set", () => { + const input = new Set(); + input.add("a"); + input.add("b"); + input.add("c"); + const cloned = structuredCloneFn(input); + expect(cloned).toBeInstanceOf(Set); + expect(cloned).not.toBe(input); + expect(cloned.size).toEqual(input.size); + for (const value of input) { + expect(cloned.has(value)).toBe(true); + } + }); + + describe("bun blobs work", () => { + test("simple", async () => { + const blob = new Blob(["hello"], { type: "application/octet-stream" }); + const cloned = structuredCloneFn(blob); + await compareBlobs(blob, cloned); + }); + test("empty", async () => { + const emptyBlob = new Blob([], { type: "" }); + const clonedEmpty = structuredCloneFn(emptyBlob); + await compareBlobs(emptyBlob, clonedEmpty); + }); + test("empty with type", async () => { + const emptyBlob = new Blob([], { type: "application/octet-stream" }); + const clonedEmpty = structuredCloneFn(emptyBlob); + await compareBlobs(emptyBlob, clonedEmpty); + }); + test("unknown type", async () => { + const blob = new Blob(["hello type"], { type: "this is type" }); + const cloned = structuredCloneFn(blob); + await compareBlobs(blob, cloned); + }); + test("file from path", async () => { + const blob = Bun.file(join(import.meta.dir, "example.txt")); + const cloned = structuredCloneFn(blob); + expect(cloned.lastModified).toBe(blob.lastModified); + expect(cloned.name).toBe(blob.name); + expect(cloned.size).toBe(blob.size); + }); + test("file from fd", async () => { + const fd = openSync(join(import.meta.dir, "example.txt"), "r"); + const blob = Bun.file(fd); + const cloned = structuredCloneFn(blob); + expect(cloned.lastModified).toBe(blob.lastModified); + expect(cloned.name).toBe(blob.name); + expect(cloned.size).toBe(blob.size); + }); + describe("dom file", async () => { + test("without lastModified", async () => { + const file = new File(["hi"], "example.txt", { type: "text/plain" }); + expect(file.lastModified).toBeGreaterThan(0); + expect(file.name).toBe("example.txt"); + expect(file.size).toBe(2); + const cloned = structuredCloneFn(file); + expect(cloned.lastModified).toBe(file.lastModified); + expect(cloned.name).toBe(file.name); + expect(cloned.size).toBe(file.size); + }); + test("with lastModified", async () => { + const file = new File(["hi"], "example.txt", { type: "text/plain", lastModified: 123 }); + expect(file.lastModified).toBe(123); + expect(file.name).toBe("example.txt"); + expect(file.size).toBe(2); + const cloned = structuredCloneFn(file); + expect(cloned.lastModified).toBe(123); + expect(cloned.name).toBe(file.name); + expect(cloned.size).toBe(file.size); + }); + }); + test("unpaired high surrogate (invalid utf-8)", async () => { + const blob = createBlob(encode_cesu8([0xd800])); + const cloned = structuredCloneFn(blob); + await compareBlobs(blob, cloned); + }); + test("unpaired low surrogate (invalid utf-8)", async () => { + const blob = createBlob(encode_cesu8([0xdc00])); + const cloned = structuredCloneFn(blob); + await compareBlobs(blob, cloned); + }); + test("paired surrogates (invalid utf-8)", async () => { + const blob = createBlob(encode_cesu8([0xd800, 0xdc00])); + const cloned = structuredCloneFn(blob); + await compareBlobs(blob, cloned); }); }); - test("unpaired high surrogate (invalid utf-8)", async () => { - const blob = createBlob(encode_cesu8([0xd800])); - const cloned = structuredClone(blob); - await compareBlobs(blob, cloned); - }); - test("unpaired low surrogate (invalid utf-8)", async () => { - const blob = createBlob(encode_cesu8([0xdc00])); - const cloned = structuredClone(blob); - await compareBlobs(blob, cloned); - }); - test("paired surrogates (invalid utf-8)", async () => { - const blob = createBlob(encode_cesu8([0xd800, 0xdc00])); - const cloned = structuredClone(blob); - await compareBlobs(blob, cloned); - }); - }); - describe("net.BlockList works", () => { - test("simple", () => { - const net = require("node:net"); - const blocklist = new net.BlockList(); - blocklist.addAddress("123.123.123.123"); - const newlist = structuredClone(blocklist); - expect(newlist.check("123.123.123.123")).toBeTrue(); - expect(!newlist.check("123.123.123.124")).toBeTrue(); - newlist.addAddress("123.123.123.124"); - expect(blocklist.check("123.123.123.124")).toBeTrue(); - expect(newlist.check("123.123.123.124")).toBeTrue(); - }); - }); + if (structuredCloneFn === structuredClone) { + describe("net.BlockList works", () => { + test("simple", () => { + const net = require("node:net"); + const blocklist = new net.BlockList(); + blocklist.addAddress("123.123.123.123"); + const newlist = structuredCloneFn(blocklist); + expect(newlist.check("123.123.123.123")).toBeTrue(); + expect(!newlist.check("123.123.123.124")).toBeTrue(); + newlist.addAddress("123.123.123.124"); + expect(blocklist.check("123.123.123.124")).toBeTrue(); + expect(newlist.check("123.123.123.124")).toBeTrue(); + }); + }); - describe("transferables", () => { - test("ArrayBuffer", () => { - const buffer = Uint8Array.from([1]).buffer; - const cloned = structuredClone(buffer, { transfer: [buffer] }); - expect(buffer.byteLength).toBe(0); - expect(cloned.byteLength).toBe(1); - }); - test("A detached ArrayBuffer cannot be transferred", () => { - const buffer = new ArrayBuffer(2); - structuredClone(buffer, { transfer: [buffer] }); - expect(() => { - structuredClone(buffer, { transfer: [buffer] }); - }).toThrow(DOMException); - }); - test("Transferring a non-transferable platform object fails", () => { - const blob = new Blob(); - expect(() => { - structuredClone(blob, { transfer: [blob] }); - }).toThrow(DOMException); - }); + describe("transferables", () => { + test("ArrayBuffer", () => { + const buffer = Uint8Array.from([1]).buffer; + const cloned = structuredCloneFn(buffer, { transfer: [buffer] }); + expect(buffer.byteLength).toBe(0); + expect(cloned.byteLength).toBe(1); + }); + test("A detached ArrayBuffer cannot be transferred", () => { + const buffer = new ArrayBuffer(2); + structuredCloneFn(buffer, { transfer: [buffer] }); + expect(() => { + structuredCloneFn(buffer, { transfer: [buffer] }); + }).toThrow(DOMException); + }); + test("Transferring a non-transferable platform object fails", () => { + const blob = new Blob(); + expect(() => { + structuredCloneFn(blob, { transfer: [blob] }); + }).toThrow(DOMException); + }); + }); + } }); -}); +} async function compareBlobs(original: Blob, cloned: Blob) { expect(cloned).toBeInstanceOf(Blob); diff --git a/test/js/web/workers/structuredClone-classes.test.ts b/test/js/web/workers/structuredClone-classes.test.ts new file mode 100644 index 0000000000..eff435ddf8 --- /dev/null +++ b/test/js/web/workers/structuredClone-classes.test.ts @@ -0,0 +1,136 @@ +import { structuredCloneAdvanced } from "bun:internal-for-testing"; +import { deserialize, serialize } from "bun:jsc"; +import { bunEnv, bunExe } from "harness"; + +enum TransferMode { + no = 0, + yes_in_transfer_list = 1, + yes_but_not_in_transfer_list = 2, +} + +const testTypes = [ + { + name: "ArrayBuffer (transferable)", + createValue: () => { + const buf = Uint8Array.from([21, 11, 96, 126, 243, 128, 164]); + return buf.buffer.transfer(); + }, + isTransferable: true, + expectedAfterClone: (original: ArrayBuffer, cloned: any, isTransfer: TransferMode, isStorage: boolean) => { + expect(cloned).toBeInstanceOf(ArrayBuffer); + expect(new Uint8Array(cloned)).toStrictEqual(new Uint8Array([21, 11, 96, 126, 243, 128, 164])); + if (isTransfer === TransferMode.yes_in_transfer_list) { + // Original should be detached after transfer + expect(original.byteLength).toBe(0); + } + }, + }, + { + name: "BunFile (cloneable, non-transferable)", + createValue: () => Bun.file(import.meta.filename), + isTransferable: false, + expectedAfterClone: (original: any, cloned: any, isTransfer: TransferMode, isStorage: boolean) => { + expect(original).toBeInstanceOf(Blob); + expect(original.name).toEqual(import.meta.filename); + expect(original.type).toEqual("text/javascript;charset=utf-8"); + + if (isTransfer || isStorage) { + // Non-transferable types should yield an empty object when transferred + expect(cloned).toBeEmptyObject(); + } else { + // When not stored or transferred, BunFile maintains its properties + expect(cloned.name).toBe(original.name); + expect(cloned.type).toBe(original.type); + } + }, + }, + { + name: "net.BlockList (cloneable, non-transferable)", + createValue: () => { + const { BlockList } = require("net"); + const blocklist = new BlockList(); + blocklist.addAddress("123.123.123.123"); + return blocklist; + }, + isTransferable: false, + expectedAfterClone: (original: any, cloned: any, isTransfer: TransferMode, isStorage: boolean) => { + if (isStorage || isTransfer !== TransferMode.no) { + // BlockList loses its internal state when stored + expect(cloned.rules).toBeUndefined(); + expect(cloned).toBeEmptyObject(); + } else { + // When not stored or transferred, BlockList maintains its properties + expect(cloned).toHaveProperty("rules"); + expect(cloned.check("123.123.123.123")).toBe(true); + } + }, + }, +]; + +describe("serialize & deserialize", () => { + for (const testType of testTypes) { + test(`${testType.name}`, async () => { + const original = testType.createValue(); + const serialized = serialize(original); + + const result = Bun.spawnSync({ + cmd: [ + bunExe(), + "-e", + ` + import {deserialize, serialize} from "bun:jsc"; + const serialized = deserialize(await Bun.stdin.bytes()); + const cloned = serialize(serialized); + process.stdout.write(cloned); + `, + ], + env: bunEnv, + stdin: serialized, + stdout: "pipe", + stderr: "inherit", + }); + const cloned = deserialize(result.stdout); + testType.expectedAfterClone(original, cloned, TransferMode.no, true); + }); + } +}); + +const contexts = ["default", "worker", "window"] as const; +const transferModes = [ + TransferMode.yes_but_not_in_transfer_list, + TransferMode.yes_in_transfer_list, + TransferMode.no, +] as const; +const storageModes = [true, false] as const; + +for (const testType of testTypes) { + for (const context of contexts) { + for (const isForTransfer of transferModes) { + for (const isForStorage of storageModes) { + test(`${testType.name} - context: ${context}, transfer: ${TransferMode[isForTransfer]}, storage: ${isForStorage}`, () => { + const original = testType.createValue(); + + if (isForTransfer === TransferMode.yes_in_transfer_list) { + // Test with transfer list (even for non-transferable types) + const transferList = [original]; + if (!testType.isTransferable) { + expect(() => + structuredCloneAdvanced(original, transferList, !!isForTransfer, isForStorage, context), + ).toThrowError("The object can not be cloned."); + } else { + const cloned = structuredCloneAdvanced(original, transferList, !!isForTransfer, isForStorage, context); + testType.expectedAfterClone(original, cloned, isForTransfer, isForStorage); + } + } else if (isForTransfer === TransferMode.yes_but_not_in_transfer_list) { + const cloned = structuredCloneAdvanced(original, [], !!isForTransfer, isForStorage, context); + testType.expectedAfterClone(original, cloned, isForTransfer, isForStorage); + } else { + // Test without transfer list + const cloned = structuredCloneAdvanced(original, [], !!isForTransfer, isForStorage, context); + testType.expectedAfterClone(original, cloned, isForTransfer, isForStorage); + } + }); + } + } + } +}