mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 18:25:13 +08:00
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> PR-URL: https://github.com/nodejs/node/pull/63933 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Filip Skokan <panva.ip@gmail.com>
124 lines
3.7 KiB
JavaScript
124 lines
3.7 KiB
JavaScript
// Flags: --expose-internals
|
|
'use strict';
|
|
|
|
// Regression tests for the native `advanced` IPC codec (see PR #63933).
|
|
// Host-object classification and tag validation must match the previous
|
|
// JavaScript (v8.DefaultSerializer-based) behavior exactly.
|
|
const common = require('../common');
|
|
const assert = require('assert');
|
|
const { fork } = require('child_process');
|
|
const v8 = require('v8');
|
|
const { MessageChannel } = require('worker_threads');
|
|
const { internalBinding } = require('internal/test/binding');
|
|
|
|
if (process.argv[2] === 'inspect') {
|
|
process.on('message', (value) => {
|
|
process.send({
|
|
isBuffer: Buffer.isBuffer(value),
|
|
keys: Object.keys(value),
|
|
visible: value?.visible,
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
|
|
function inspect(value) {
|
|
return new Promise((resolve, reject) => {
|
|
const child = fork(__filename, ['inspect'], {
|
|
serialization: 'advanced',
|
|
stdio: ['ignore', 'ignore', 'inherit', 'ipc'],
|
|
});
|
|
child.once('message', (message) => {
|
|
child.disconnect();
|
|
resolve(message);
|
|
});
|
|
try {
|
|
child.send(value);
|
|
} catch (err) {
|
|
child.disconnect();
|
|
reject(err);
|
|
}
|
|
});
|
|
}
|
|
|
|
async function main() {
|
|
// 1) Spreading a non-ArrayBufferView host object must copy own properties
|
|
// with [[DefineOwnProperty]] semantics, i.e. without invoking inherited
|
|
// setters (the JS codec used `{ ...object }`).
|
|
{
|
|
const { port1, port2 } = new MessageChannel();
|
|
port1.visible = 1;
|
|
Object.defineProperty(Object.prototype, 'visible', {
|
|
configurable: true,
|
|
get() { return undefined; },
|
|
set() { throw new Error('setter called'); },
|
|
});
|
|
let message;
|
|
try {
|
|
message = await inspect(port1);
|
|
} finally {
|
|
delete Object.prototype.visible;
|
|
port1.close();
|
|
port2.close();
|
|
}
|
|
assert.strictEqual(message.visible, 1);
|
|
assert.strictEqual(message.keys[0], 'visible');
|
|
}
|
|
|
|
// 2) A Buffer whose `.constructor` is reassigned is classified by its
|
|
// constructor, exactly like v8.DefaultSerializer: it is no longer a
|
|
// Buffer on the receiving side.
|
|
{
|
|
const buf = Buffer.from('abc');
|
|
buf.constructor = Uint8Array;
|
|
const message = await inspect(buf);
|
|
assert.strictEqual(message.isBuffer, false);
|
|
}
|
|
|
|
// 3) Conversely, a Uint8Array with `.constructor === Buffer` round-trips as
|
|
// a Buffer.
|
|
{
|
|
const uint8 = new Uint8Array([1, 2, 3]);
|
|
uint8.constructor = Buffer;
|
|
const message = await inspect(uint8);
|
|
assert.strictEqual(message.isBuffer, true);
|
|
}
|
|
|
|
// 4) Tampering with Buffer.prototype.constructor must not fool the
|
|
// classifier: a genuine Buffer is then classified by its (now non-Buffer)
|
|
// constructor, matching v8.DefaultSerializer, which compares against a
|
|
// stable Buffer reference rather than re-reading Buffer.prototype.
|
|
{
|
|
const original = Buffer.prototype.constructor;
|
|
Buffer.prototype.constructor = Uint8Array;
|
|
let message;
|
|
try {
|
|
message = await inspect(Buffer.from('abc'));
|
|
} finally {
|
|
Buffer.prototype.constructor = original;
|
|
}
|
|
assert.strictEqual(message.isBuffer, false);
|
|
}
|
|
|
|
// 5) A host-object tag other than the two the codec emits (0 and 1) must be
|
|
// rejected rather than silently accepted.
|
|
{
|
|
const { deserialize } = internalBinding('ipc_serdes');
|
|
|
|
class BadTagSerializer extends v8.DefaultSerializer {
|
|
_writeHostObject(value) {
|
|
this.writeUint32(2); // Valid tags are only 0 or 1
|
|
return super._writeHostObject(value);
|
|
}
|
|
}
|
|
const ser = new BadTagSerializer();
|
|
ser.writeHeader();
|
|
ser.writeValue(Buffer.from('x'));
|
|
const payload = ser.releaseBuffer();
|
|
|
|
assert.throws(() => deserialize(payload), { code: 'ERR_INVALID_STATE' });
|
|
}
|
|
}
|
|
|
|
main().then(common.mustCall());
|