Skip to content

Commit ef73d8f

Browse files
committed
child_process: serialize advanced IPC messages natively
The `advanced` IPC serialization codec was implemented in JavaScript (ChildProcessSerializer / ChildProcessDeserializer in lib/internal/child_process/serialization.js). It allocated a wrapper serializer/deserializer per message and crossed the JS/C++ boundary several times for every message (writeHeader, writeValue, releaseBuffer, readHeader, readValue and friends). Move the codec into a native `ipc_serdes` binding that drives the V8 ValueSerializer/ValueDeserializer with a C++ delegate. The wire format is preserved byte-for-byte: a big-endian uint32 length prefix followed by the V8 payload, with ArrayBufferViews tagged as host objects so that Node Buffers round-trip as Buffers rather than plain Uint8Arrays. The JSON codec is left unchanged. A cctest (test/cctest/test_node_ipc_serdes.cc) exercises the binding directly, covering round-trips of primitives, objects, typed arrays and Buffers (including the Buffer-vs-Uint8Array distinction) and asserting the big-endian length-prefix framing. Round-trip throughput (benchmark/child_process/child-process-ipc-roundtrip): payload before after change 64 B ~300k/s ~800k/s +166% 1 KiB ~272k/s ~616k/s +126% 16 KiB ~91k/s ~120k/s +32% 64 KiB ~30k/s ~35k/s +16% The gain is largest for small messages, where per-message JavaScript overhead dominated, and tapers for large messages, where the actual serialization (already native) dominates. Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
1 parent 3f42cfa commit ef73d8f

9 files changed

Lines changed: 712 additions & 50 deletions

File tree

lib/internal/child_process/serialization.js

Lines changed: 5 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -10,43 +10,14 @@ const {
1010
} = primordials;
1111
const { Buffer } = require('buffer');
1212
const { StringDecoder } = require('string_decoder');
13-
const v8 = require('v8');
14-
const { isArrayBufferView } = require('internal/util/types');
15-
const assert = require('internal/assert');
13+
const { serialize, deserialize } = internalBinding('ipc_serdes');
1614
const { streamBaseState, kLastWriteWasAsync } = internalBinding('stream_wrap');
1715

1816
const kMessageBuffer = Symbol('kMessageBuffer');
1917
const kMessageBufferSize = Symbol('kMessageBufferSize');
2018
const kJSONBuffer = Symbol('kJSONBuffer');
2119
const kStringDecoder = Symbol('kStringDecoder');
2220

23-
// Extend V8's serializer APIs to give more JSON-like behaviour in
24-
// some cases; in particular, for native objects this serializes them the same
25-
// way that JSON does rather than throwing an exception.
26-
const kArrayBufferViewTag = 0;
27-
const kNotArrayBufferViewTag = 1;
28-
class ChildProcessSerializer extends v8.DefaultSerializer {
29-
_writeHostObject(object) {
30-
if (isArrayBufferView(object)) {
31-
this.writeUint32(kArrayBufferViewTag);
32-
return super._writeHostObject(object);
33-
}
34-
this.writeUint32(kNotArrayBufferViewTag);
35-
this.writeValue({ ...object });
36-
}
37-
}
38-
39-
class ChildProcessDeserializer extends v8.DefaultDeserializer {
40-
_readHostObject() {
41-
const tag = this.readUint32();
42-
if (tag === kArrayBufferViewTag)
43-
return super._readHostObject();
44-
45-
assert(tag === kNotArrayBufferViewTag);
46-
return this.readValue();
47-
}
48-
}
49-
5021
// Messages are parsed in either of the following formats:
5122
// - Newline-delimited JSON, or
5223
// - V8-serialized buffers, prefixed with their length as a big endian uint32
@@ -90,38 +61,22 @@ const advanced = {
9061
channel[kMessageBufferSize],
9162
);
9263

93-
const deserializer = new ChildProcessDeserializer(
94-
TypedArrayPrototypeSubarray(concatenatedBuffer, 4, fullMessageSize),
95-
);
64+
const serializedMessage =
65+
TypedArrayPrototypeSubarray(concatenatedBuffer, 4, fullMessageSize);
9666

9767
messageBufferHead = TypedArrayPrototypeSubarray(concatenatedBuffer, fullMessageSize);
9868
channel[kMessageBufferSize] = messageBufferHead.length;
9969
channel[kMessageBuffer] =
10070
channel[kMessageBufferSize] !== 0 ? [messageBufferHead] : [];
10171

102-
deserializer.readHeader();
103-
yield deserializer.readValue();
72+
yield deserialize(serializedMessage);
10473
}
10574

10675
channel.buffering = channel[kMessageBufferSize] > 0;
10776
},
10877

10978
writeChannelMessage(channel, req, message, handle) {
110-
const ser = new ChildProcessSerializer();
111-
// Add 4 bytes, to later populate with message length
112-
ser.writeRawBytes(Buffer.allocUnsafe(4));
113-
ser.writeHeader();
114-
ser.writeValue(message);
115-
116-
const serializedMessage = ser.releaseBuffer();
117-
const serializedMessageLength = serializedMessage.length - 4;
118-
119-
serializedMessage.set([
120-
serializedMessageLength >> 24 & 0xFF,
121-
serializedMessageLength >> 16 & 0xFF,
122-
serializedMessageLength >> 8 & 0xFF,
123-
serializedMessageLength & 0xFF,
124-
], 0);
79+
const serializedMessage = serialize(message);
12580

12681
const result = channel.writeBuffer(req, serializedMessage, handle);
12782

node.gyp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@
136136
'src/node_http_parser.cc',
137137
'src/node_http2.cc',
138138
'src/node_i18n.cc',
139+
'src/node_ipc_serdes.cc',
139140
'src/node_locks.cc',
140141
'src/node_main_instance.cc',
141142
'src/node_messaging.cc',

src/node_binding.cc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
V(http_parser) \
6060
V(inspector) \
6161
V(internal_only_v8) \
62+
V(ipc_serdes) \
6263
V(js_stream) \
6364
V(js_udp_wrap) \
6465
V(locks) \

src/node_external_reference.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ class ExternalReferenceRegistry {
8484
V(heap_utils) \
8585
V(http_parser) \
8686
V(internal_only_v8) \
87+
V(ipc_serdes) \
8788
V(locks) \
8889
V(messaging) \
8990
V(mksnapshot) \

0 commit comments

Comments
 (0)