Skip to content

Commit e51beef

Browse files
addaleaxMylesBorins
authored andcommitted
child_process,cluster: allow using V8 serialization API
Add an `serialization` option that allows child process IPC to use the (typically more powerful) V8 serialization API. Fixes: #10965 PR-URL: #30162 Reviewed-By: Colin Ihrig <[email protected]> Reviewed-By: Gus Caplan <[email protected]> Reviewed-By: David Carlier <[email protected]> Reviewed-By: Michaël Zasso <[email protected]>
1 parent 7b41874 commit e51beef

13 files changed

+302
-38
lines changed

benchmark/cluster/echo.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,25 @@ if (cluster.isMaster) {
77
workers: [1],
88
payload: ['string', 'object'],
99
sendsPerBroadcast: [1, 10],
10+
serialization: ['json', 'advanced'],
1011
n: [1e5]
1112
});
1213

13-
function main({ n, workers, sendsPerBroadcast, payload }) {
14+
function main({
15+
n,
16+
workers,
17+
sendsPerBroadcast,
18+
payload,
19+
serialization
20+
}) {
1421
const expectedPerBroadcast = sendsPerBroadcast * workers;
1522
var readies = 0;
1623
var broadcasts = 0;
1724
var msgCount = 0;
1825
var data;
1926

27+
cluster.settings.serialization = serialization;
28+
2029
switch (payload) {
2130
case 'string':
2231
data = 'hello world!';

doc/api/child_process.md

+39
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,9 @@ arbitrary command execution.**
321321
<!-- YAML
322322
added: v0.5.0
323323
changes:
324+
- version: REPLACEME
325+
pr-url: https://github.com/nodejs/node/pull/30162
326+
description: The `serialization` option is supported now.
324327
- version: v8.0.0
325328
pr-url: https://github.com/nodejs/node/pull/10866
326329
description: The `stdio` option can now be a string.
@@ -340,6 +343,9 @@ changes:
340343
* `execPath` {string} Executable used to create the child process.
341344
* `execArgv` {string[]} List of string arguments passed to the executable.
342345
**Default:** `process.execArgv`.
346+
* `serialization` {string} Specify the kind of serialization used for sending
347+
messages between processes. Possible values are `'json'` and `'advanced'`.
348+
See [Advanced Serialization][] for more details. **Default:** `'json'`.
343349
* `silent` {boolean} If `true`, stdin, stdout, and stderr of the child will be
344350
piped to the parent, otherwise they will be inherited from the parent, see
345351
the `'pipe'` and `'inherit'` options for [`child_process.spawn()`][]'s
@@ -386,6 +392,9 @@ The `shell` option available in [`child_process.spawn()`][] is not supported by
386392
<!-- YAML
387393
added: v0.1.90
388394
changes:
395+
- version: REPLACEME
396+
pr-url: https://github.com/nodejs/node/pull/30162
397+
description: The `serialization` option is supported now.
389398
- version: v8.8.0
390399
pr-url: https://github.com/nodejs/node/pull/15380
391400
description: The `windowsHide` option is supported now.
@@ -411,6 +420,9 @@ changes:
411420
[`options.detached`][]).
412421
* `uid` {number} Sets the user identity of the process (see setuid(2)).
413422
* `gid` {number} Sets the group identity of the process (see setgid(2)).
423+
* `serialization` {string} Specify the kind of serialization used for sending
424+
messages between processes. Possible values are `'json'` and `'advanced'`.
425+
See [Advanced Serialization][] for more details. **Default:** `'json'`.
414426
* `shell` {boolean|string} If `true`, runs `command` inside of a shell. Uses
415427
`'/bin/sh'` on Unix, and `process.env.ComSpec` on Windows. A different
416428
shell can be specified as a string. See [Shell Requirements][] and
@@ -998,6 +1010,11 @@ The `'message'` event is triggered when a child process uses
9981010
The message goes through serialization and parsing. The resulting
9991011
message might not be the same as what is originally sent.
10001012

1013+
If the `serialization` option was set to `'advanced'` used when spawning the
1014+
child process, the `message` argument can contain data that JSON is not able
1015+
to represent.
1016+
See [Advanced Serialization][] for more details.
1017+
10011018
### subprocess.channel
10021019
<!-- YAML
10031020
added: v7.1.0
@@ -1472,6 +1489,26 @@ the same requirement. Thus, in `child_process` functions where a shell can be
14721489
spawned, `'cmd.exe'` is used as a fallback if `process.env.ComSpec` is
14731490
unavailable.
14741491

1492+
## Advanced Serialization
1493+
<!-- YAML
1494+
added: REPLACEME
1495+
-->
1496+
1497+
Child processes support a serialization mechanism for IPC that is based on the
1498+
[serialization API of the `v8` module][v8.serdes], based on the
1499+
[HTML structured clone algorithm][]. This is generally more powerful and
1500+
supports more built-in JavaScript object types, such as `BigInt`, `Map`
1501+
and `Set`, `ArrayBuffer` and `TypedArray`, `Buffer`, `Error`, `RegExp` etc.
1502+
1503+
However, this format is not a full superset of JSON, and e.g. properties set on
1504+
objects of such built-in types will not be passed on through the serialization
1505+
step. Additionally, performance may not be equivalent to that of JSON, depending
1506+
on the structure of the passed data.
1507+
Therefore, this feature requires opting in by setting the
1508+
`serialization` option to `'advanced'` when calling [`child_process.spawn()`][]
1509+
or [`child_process.fork()`][].
1510+
1511+
[Advanced Serialization]: #child_process_advanced_serialization
14751512
[`'disconnect'`]: process.html#process_event_disconnect
14761513
[`'error'`]: #child_process_event_error
14771514
[`'exit'`]: #child_process_event_exit
@@ -1505,5 +1542,7 @@ unavailable.
15051542
[`subprocess.stdout`]: #child_process_subprocess_stdout
15061543
[`util.promisify()`]: util.html#util_util_promisify_original
15071544
[Default Windows Shell]: #child_process_default_windows_shell
1545+
[HTML structured clone algorithm]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
15081546
[Shell Requirements]: #child_process_shell_requirements
15091547
[synchronous counterparts]: #child_process_synchronous_process_creation
1548+
[v8.serdes]: v8.html#v8_serialization_api

doc/api/cluster.md

+8
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,9 @@ values are `'rr'` and `'none'`.
724724
<!-- YAML
725725
added: v0.7.1
726726
changes:
727+
- version: REPLACEME
728+
pr-url: https://github.com/nodejs/node/pull/30162
729+
description: The `serialization` option is supported now.
727730
- version: v9.5.0
728731
pr-url: https://github.com/nodejs/node/pull/18399
729732
description: The `cwd` option is supported now.
@@ -746,6 +749,10 @@ changes:
746749
**Default:** `process.argv.slice(2)`.
747750
* `cwd` {string} Current working directory of the worker process. **Default:**
748751
`undefined` (inherits from parent process).
752+
* `serialization` {string} Specify the kind of serialization used for sending
753+
messages between processes. Possible values are `'json'` and `'advanced'`.
754+
See [Advanced Serialization for `child_process`][] for more details.
755+
**Default:** `false`.
749756
* `silent` {boolean} Whether or not to send output to parent's stdio.
750757
**Default:** `false`.
751758
* `stdio` {Array} Configures the stdio of forked processes. Because the
@@ -874,4 +881,5 @@ socket.on('data', (id) => {
874881
[`process` event: `'message'`]: process.html#process_event_message
875882
[`server.close()`]: net.html#net_event_close
876883
[`worker.exitedAfterDisconnect`]: #cluster_worker_exitedafterdisconnect
884+
[Advanced Serialization for `child_process`]: child_process.html#child_process_advanced_serialization
877885
[Child Process module]: child_process.html#child_process_child_process_fork_modulepath_args_options

doc/api/process.md

+6
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@ the child process.
119119
The message goes through serialization and parsing. The resulting message might
120120
not be the same as what is originally sent.
121121

122+
If the `serialization` option was set to `advanced` used when spawning the
123+
process, the `message` argument can contain data that JSON is not able
124+
to represent.
125+
See [Advanced Serialization for `child_process`][] for more details.
126+
122127
### Event: 'multipleResolves'
123128
<!-- YAML
124129
added: v10.12.0
@@ -2456,6 +2461,7 @@ cases:
24562461
[`require.resolve()`]: modules.html#modules_require_resolve_request_options
24572462
[`subprocess.kill()`]: child_process.html#child_process_subprocess_kill_signal
24582463
[`v8.setFlagsFromString()`]: v8.html#v8_v8_setflagsfromstring_flags
2464+
[Advanced Serialization for `child_process`]: child_process.html#child_process_advanced_serialization
24592465
[Android building]: https://github.com/nodejs/node/blob/master/BUILDING.md#androidandroid-based-devices-eg-firefox-os
24602466
[Child Process]: child_process.html
24612467
[Cluster]: cluster.html

lib/child_process.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,12 @@ function fork(modulePath /* , args, options */) {
108108
return spawn(options.execPath, args, options);
109109
}
110110

111-
function _forkChild(fd) {
111+
function _forkChild(fd, serializationMode) {
112112
// set process.send()
113113
const p = new Pipe(PipeConstants.IPC);
114114
p.open(fd);
115115
p.unref();
116-
const control = setupChannel(process, p);
116+
const control = setupChannel(process, p, serializationMode);
117117
process.on('newListener', function onNewListener(name) {
118118
if (name === 'message' || name === 'disconnect') control.ref();
119119
});

lib/internal/bootstrap/pre_execution.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,11 @@ function setupChildProcessIpcChannel() {
326326
// Make sure it's not accidentally inherited by child processes.
327327
delete process.env.NODE_CHANNEL_FD;
328328

329-
require('child_process')._forkChild(fd);
329+
const serializationMode =
330+
process.env.NODE_CHANNEL_SERIALIZATION_MODE || 'json';
331+
delete process.env.NODE_CHANNEL_SERIALIZATION_MODE;
332+
333+
require('child_process')._forkChild(fd, serializationMode);
330334
assert(process.send);
331335
}
332336
}

lib/internal/child_process.js

+27-32
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use strict';
22

3-
const { JSON, Object } = primordials;
3+
const { Object } = primordials;
44

55
const {
66
errnoException,
@@ -55,8 +55,6 @@ const {
5555

5656
const { SocketListSend, SocketListReceive } = SocketList;
5757

58-
// Lazy loaded for startup performance.
59-
let StringDecoder;
6058
// Lazy loaded for startup performance and to allow monkey patching of
6159
// internalBinding('http_parser').HTTPParser.
6260
let freeParser;
@@ -343,6 +341,15 @@ ChildProcess.prototype.spawn = function(options) {
343341
const ipcFd = stdio.ipcFd;
344342
stdio = options.stdio = stdio.stdio;
345343

344+
if (options.serialization !== undefined &&
345+
options.serialization !== 'json' &&
346+
options.serialization !== 'advanced') {
347+
throw new ERR_INVALID_OPT_VALUE('options.serialization',
348+
options.serialization);
349+
}
350+
351+
const serialization = options.serialization || 'json';
352+
346353
if (ipc !== undefined) {
347354
// Let child process know about opened IPC channel
348355
if (options.envPairs === undefined)
@@ -353,7 +360,8 @@ ChildProcess.prototype.spawn = function(options) {
353360
options.envPairs);
354361
}
355362

356-
options.envPairs.push('NODE_CHANNEL_FD=' + ipcFd);
363+
options.envPairs.push(`NODE_CHANNEL_FD=${ipcFd}`);
364+
options.envPairs.push(`NODE_CHANNEL_SERIALIZATION_MODE=${serialization}`);
357365
}
358366

359367
validateString(options.file, 'options.file');
@@ -446,7 +454,7 @@ ChildProcess.prototype.spawn = function(options) {
446454
this.stdio.push(stdio[i].socket === undefined ? null : stdio[i].socket);
447455

448456
// Add .send() method and start listening for IPC data
449-
if (ipc !== undefined) setupChannel(this, ipc);
457+
if (ipc !== undefined) setupChannel(this, ipc, serialization);
450458

451459
return err;
452460
};
@@ -516,7 +524,8 @@ class Control extends EventEmitter {
516524
const channelDeprecationMsg = '_channel is deprecated. ' +
517525
'Use ChildProcess.channel instead.';
518526

519-
function setupChannel(target, channel) {
527+
let serialization;
528+
function setupChannel(target, channel, serializationMode) {
520529
target.channel = channel;
521530

522531
Object.defineProperty(target, '_channel', {
@@ -535,12 +544,16 @@ function setupChannel(target, channel) {
535544

536545
const control = new Control(channel);
537546

538-
if (StringDecoder === undefined)
539-
StringDecoder = require('string_decoder').StringDecoder;
540-
const decoder = new StringDecoder('utf8');
541-
var jsonBuffer = '';
542-
var pendingHandle = null;
543-
channel.buffering = false;
547+
if (serialization === undefined)
548+
serialization = require('internal/child_process/serialization');
549+
const {
550+
initMessageChannel,
551+
parseChannelMessages,
552+
writeChannelMessage
553+
} = serialization[serializationMode];
554+
555+
let pendingHandle = null;
556+
initMessageChannel(channel);
544557
channel.pendingHandle = null;
545558
channel.onread = function(arrayBuffer) {
546559
const recvHandle = channel.pendingHandle;
@@ -552,21 +565,7 @@ function setupChannel(target, channel) {
552565
if (recvHandle)
553566
pendingHandle = recvHandle;
554567

555-
// Linebreak is used as a message end sign
556-
var chunks = decoder.write(pool).split('\n');
557-
var numCompleteChunks = chunks.length - 1;
558-
// Last line does not have trailing linebreak
559-
var incompleteChunk = chunks[numCompleteChunks];
560-
if (numCompleteChunks === 0) {
561-
jsonBuffer += incompleteChunk;
562-
this.buffering = jsonBuffer.length !== 0;
563-
return;
564-
}
565-
chunks[0] = jsonBuffer + chunks[0];
566-
567-
for (var i = 0; i < numCompleteChunks; i++) {
568-
var message = JSON.parse(chunks[i]);
569-
568+
for (const message of parseChannelMessages(channel, pool)) {
570569
// There will be at most one NODE_HANDLE message in every chunk we
571570
// read because SCM_RIGHTS messages don't get coalesced. Make sure
572571
// that we deliver the handle with the right message however.
@@ -581,9 +580,6 @@ function setupChannel(target, channel) {
581580
handleMessage(message, undefined, false);
582581
}
583582
}
584-
jsonBuffer = incompleteChunk;
585-
this.buffering = jsonBuffer.length !== 0;
586-
587583
} else {
588584
this.buffering = false;
589585
target.disconnect();
@@ -782,8 +778,7 @@ function setupChannel(target, channel) {
782778

783779
const req = new WriteWrap();
784780

785-
const string = JSON.stringify(message) + '\n';
786-
const err = channel.writeUtf8String(req, string, handle);
781+
const err = writeChannelMessage(channel, req, message, handle);
787782
const wasAsyncWrite = streamBaseState[kLastWriteWasAsync];
788783

789784
if (err === 0) {

0 commit comments

Comments
 (0)