Skip to content

Commit

Permalink
src: implement structuredClone in native
Browse files Browse the repository at this point in the history
Simplify the implementation by implementing it directly in C++.
This improves performance and also makes structuredClone supported
in custom snapshots.

PR-URL: #50330
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: Daeyeon Jeong <daeyeon.dev@gmail.com>
  • Loading branch information
joyeecheung authored and UlisesGascon committed Dec 11, 2023
1 parent d4be8fa commit 4ef1d68
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 82 deletions.
46 changes: 46 additions & 0 deletions benchmark/misc/structured-clone.js
@@ -0,0 +1,46 @@
'use strict';

const common = require('../common.js');
const assert = require('assert');

const bench = common.createBenchmark(main, {
type: ['string', 'object', 'arraybuffer'],
n: [1e4],
});

function main({ n, type }) {
const data = [];

switch (type) {
case 'string':
for (let i = 0; i < n; ++i) {
data.push(new Date().toISOString());
}
break;
case 'object':
for (let i = 0; i < n; ++i) {
data.push({ ...process.config });
}
break;
case 'arraybuffer':
for (let i = 0; i < n; ++i) {
data.push(new ArrayBuffer(10));
}
break;
default:
throw new Error('Unsupported payload type');
}

const run = type === 'arraybuffer' ? (i) => {
data[i] = structuredClone(data[i], { transfer: [ data[i] ] });
} : (i) => {
data[i] = structuredClone(data[i]);
};

bench.start();
for (let i = 0; i < n; ++i) {
run(i);
}
bench.end(n);
assert.strictEqual(data.length, n);
}
2 changes: 1 addition & 1 deletion lib/.eslintrc.yaml
Expand Up @@ -172,7 +172,7 @@ rules:
- name: setTimeout
message: Use `const { setTimeout } = require('timers');` instead of the global.
- name: structuredClone
message: Use `const { structuredClone } = require('internal/structured_clone');` instead of the global.
message: Use `const { structuredClone } = internalBinding('messaging');` instead of the global.
- name: SubtleCrypto
message: Use `const { SubtleCrypto } = require('internal/crypto/webcrypto');` instead of the global.
no-restricted-modules:
Expand Down
7 changes: 2 additions & 5 deletions lib/internal/bootstrap/web/exposed-window-or-worker.js
Expand Up @@ -31,11 +31,8 @@ const {
} = require('internal/process/task_queues');
defineOperation(globalThis, 'queueMicrotask', queueMicrotask);

defineLazyProperties(
globalThis,
'internal/structured_clone',
['structuredClone'],
);
const { structuredClone } = internalBinding('messaging');
defineOperation(globalThis, 'structuredClone', structuredClone);
defineLazyProperties(globalThis, 'buffer', ['atob', 'btoa']);

// https://html.spec.whatwg.org/multipage/web-messaging.html#broadcasting-to-other-browsing-contexts
Expand Down
2 changes: 1 addition & 1 deletion lib/internal/perf/usertiming.js
Expand Up @@ -31,7 +31,7 @@ const {
},
} = require('internal/errors');

const { structuredClone } = require('internal/structured_clone');
const { structuredClone } = internalBinding('messaging');
const {
lazyDOMException,
kEnumerableProperty,
Expand Down
29 changes: 0 additions & 29 deletions lib/internal/structured_clone.js

This file was deleted.

4 changes: 1 addition & 3 deletions lib/internal/webstreams/readablestream.js
Expand Up @@ -86,9 +86,7 @@ const {
kControllerErrorFunction,
} = require('internal/streams/utils');

const {
structuredClone,
} = require('internal/structured_clone');
const { structuredClone } = internalBinding('messaging');

const {
ArrayBufferViewGetBuffer,
Expand Down
112 changes: 87 additions & 25 deletions src/node_messaging.cc
Expand Up @@ -999,6 +999,47 @@ static Maybe<bool> ReadIterable(Environment* env,
return Just(true);
}

bool GetTransferList(Environment* env,
Local<Context> context,
Local<Value> transfer_list_v,
TransferList* transfer_list_out) {
if (transfer_list_v->IsNullOrUndefined()) {
// Browsers ignore null or undefined, and otherwise accept an array or an
// options object.
return true;
}

if (!transfer_list_v->IsObject()) {
THROW_ERR_INVALID_ARG_TYPE(
env, "Optional transferList argument must be an iterable");
return false;
}

bool was_iterable;
if (!ReadIterable(env, context, *transfer_list_out, transfer_list_v)
.To(&was_iterable))
return false;
if (!was_iterable) {
Local<Value> transfer_option;
if (!transfer_list_v.As<Object>()
->Get(context, env->transfer_string())
.ToLocal(&transfer_option))
return false;
if (!transfer_option->IsUndefined()) {
if (!ReadIterable(env, context, *transfer_list_out, transfer_option)
.To(&was_iterable))
return false;
if (!was_iterable) {
THROW_ERR_INVALID_ARG_TYPE(
env, "Optional options.transfer argument must be an iterable");
return false;
}
}
}

return true;
}

void MessagePort::PostMessage(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Local<Object> obj = args.This();
Expand All @@ -1009,33 +1050,10 @@ void MessagePort::PostMessage(const FunctionCallbackInfo<Value>& args) {
"MessagePort.postMessage");
}

if (!args[1]->IsNullOrUndefined() && !args[1]->IsObject()) {
// Browsers ignore null or undefined, and otherwise accept an array or an
// options object.
return THROW_ERR_INVALID_ARG_TYPE(env,
"Optional transferList argument must be an iterable");
}

TransferList transfer_list;
if (args[1]->IsObject()) {
bool was_iterable;
if (!ReadIterable(env, context, transfer_list, args[1]).To(&was_iterable))
return;
if (!was_iterable) {
Local<Value> transfer_option;
if (!args[1].As<Object>()->Get(context, env->transfer_string())
.ToLocal(&transfer_option)) return;
if (!transfer_option->IsUndefined()) {
if (!ReadIterable(env, context, transfer_list, transfer_option)
.To(&was_iterable)) return;
if (!was_iterable) {
return THROW_ERR_INVALID_ARG_TYPE(env,
"Optional options.transfer argument must be an iterable");
}
}
}
if (!GetTransferList(env, context, args[1], &transfer_list)) {
return;
}

MessagePort* port = Unwrap<MessagePort>(args.This());
// Even if the backing MessagePort object has already been deleted, we still
// want to serialize the message to ensure spec-compliant behavior w.r.t.
Expand Down Expand Up @@ -1472,6 +1490,48 @@ static void SetDeserializerCreateObjectFunction(
env->set_messaging_deserialize_create_object(args[0].As<Function>());
}

static void StructuredClone(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
Realm* realm = Realm::GetCurrent(context);
Environment* env = realm->env();

if (args.Length() == 0) {
return THROW_ERR_MISSING_ARGS(env, "The value argument must be specified");
}

Local<Value> value = args[0];

TransferList transfer_list;
if (!args[1]->IsNullOrUndefined()) {
if (!args[1]->IsObject()) {
return THROW_ERR_INVALID_ARG_TYPE(
env, "The options argument must be either an object or undefined");
}
Local<Object> options = args[1].As<Object>();
Local<Value> transfer_list_v;
if (!options->Get(context, env->transfer_string())
.ToLocal(&transfer_list_v)) {
return;
}

// TODO(joyeecheung): implement this in JS land to avoid the C++ -> JS
// cost to convert a sequence into an array.
if (!GetTransferList(env, context, transfer_list_v, &transfer_list)) {
return;
}
}

std::shared_ptr<Message> msg = std::make_shared<Message>();
Local<Value> result;
if (msg->Serialize(env, context, value, transfer_list, Local<Object>())
.IsNothing() ||
!msg->Deserialize(env, context, nullptr).ToLocal(&result)) {
return;
}
args.GetReturnValue().Set(result);
}

static void MessageChannel(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
if (!args.IsConstructCall()) {
Expand Down Expand Up @@ -1554,6 +1614,7 @@ static void InitMessaging(Local<Object> target,
"setDeserializerCreateObjectFunction",
SetDeserializerCreateObjectFunction);
SetMethod(context, target, "broadcastChannel", BroadcastChannel);
SetMethod(context, target, "structuredClone", StructuredClone);

{
Local<Function> domexception = GetDOMException(context).ToLocalChecked();
Expand All @@ -1578,6 +1639,7 @@ static void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(MessagePort::ReceiveMessage);
registry->Register(MessagePort::MoveToContext);
registry->Register(SetDeserializerCreateObjectFunction);
registry->Register(StructuredClone);
}

} // anonymous namespace
Expand Down
30 changes: 12 additions & 18 deletions test/parallel/test-structuredClone-global.js
@@ -1,23 +1,17 @@
// Flags: --expose-internals
'use strict';
/* eslint-disable no-global-assign */

require('../common');
const assert = require('assert');

const {
structuredClone: _structuredClone,
} = require('internal/structured_clone');
assert.throws(() => structuredClone(), { code: 'ERR_MISSING_ARGS' });
assert.throws(() => structuredClone(undefined, ''), { code: 'ERR_INVALID_ARG_TYPE' });
assert.throws(() => structuredClone(undefined, 1), { code: 'ERR_INVALID_ARG_TYPE' });
assert.throws(() => structuredClone(undefined, { transfer: 1 }), { code: 'ERR_INVALID_ARG_TYPE' });
assert.throws(() => structuredClone(undefined, { transfer: '' }), { code: 'ERR_INVALID_ARG_TYPE' });

const {
strictEqual,
throws,
} = require('assert');

strictEqual(globalThis.structuredClone, _structuredClone);
structuredClone = undefined;
strictEqual(globalThis.structuredClone, undefined);

// Restore the value for the known globals check.
structuredClone = _structuredClone;

throws(() => _structuredClone(), /ERR_MISSING_ARGS/);
// Options can be null or undefined.
assert.strictEqual(structuredClone(undefined), undefined);
assert.strictEqual(structuredClone(undefined, null), undefined);
// Transfer can be null or undefined.
assert.strictEqual(structuredClone(undefined, { transfer: null }), undefined);
assert.strictEqual(structuredClone(undefined, { }), undefined);

0 comments on commit 4ef1d68

Please sign in to comment.