node/test/cctest/test_node_ipc_serdes.cc
Yagiz Nizipli a1074b8460
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>
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>
2026-06-18 18:14:46 +00:00

210 lines
7.7 KiB
C++

#include "node.h"
#include "node_binding.h"
#include "node_buffer.h"
#include "node_test_fixture.h"
#include <string>
using node::Environment;
namespace {
// Bootstraps the environment (so Buffer and friends are initialized) and
// returns the `ipc_serdes` internal binding object.
v8::Local<v8::Object> GetIpcSerdesBinding(Environment* env,
v8::Local<v8::Context> context) {
v8::Isolate* isolate = v8::Isolate::GetCurrent();
node::LoadEnvironment(env, "");
v8::Local<v8::Function> get_internal_binding =
v8::Function::New(context, node::binding::GetInternalBinding)
.ToLocalChecked();
v8::Local<v8::Value> binding_name =
v8::String::NewFromUtf8Literal(isolate, "ipc_serdes");
v8::Local<v8::Value> binding =
get_internal_binding
->Call(context, v8::Undefined(isolate), 1, &binding_name)
.ToLocalChecked();
return binding.As<v8::Object>();
}
v8::Local<v8::Function> GetFunction(v8::Local<v8::Context> context,
v8::Local<v8::Object> object,
const char* name) {
v8::Isolate* isolate = v8::Isolate::GetCurrent();
v8::Local<v8::Value> value =
object
->Get(context,
v8::String::NewFromUtf8(isolate, name).ToLocalChecked())
.ToLocalChecked();
return value.As<v8::Function>();
}
// Serializes `input`, validates the 4-byte big-endian length prefix, strips it
// (as parseChannelMessages does), and returns the deserialized value.
v8::Local<v8::Value> RoundTrip(v8::Local<v8::Context> context,
v8::Local<v8::Function> serialize,
v8::Local<v8::Function> deserialize,
v8::Local<v8::Value> input) {
v8::Isolate* isolate = v8::Isolate::GetCurrent();
// serialize(message, Buffer): the second argument is the stable Buffer
// constructor used to classify Node Buffers.
v8::Local<v8::Value> buffer_ctor =
context->Global()
->Get(context, v8::String::NewFromUtf8Literal(isolate, "Buffer"))
.ToLocalChecked();
v8::Local<v8::Value> serialize_args[] = {input, buffer_ctor};
v8::Local<v8::Value> serialized =
serialize->Call(context, v8::Undefined(isolate), 2, serialize_args)
.ToLocalChecked();
EXPECT_TRUE(serialized->IsUint8Array());
v8::Local<v8::Uint8Array> buf = serialized.As<v8::Uint8Array>();
EXPECT_GE(buf->ByteLength(), static_cast<size_t>(4));
node::ArrayBufferViewContents<uint8_t> bytes(serialized);
const uint32_t payload_length =
(static_cast<uint32_t>(bytes.data()[0]) << 24) |
(static_cast<uint32_t>(bytes.data()[1]) << 16) |
(static_cast<uint32_t>(bytes.data()[2]) << 8) |
static_cast<uint32_t>(bytes.data()[3]);
EXPECT_EQ(payload_length, buf->ByteLength() - 4);
v8::Local<v8::Value> payload = v8::Uint8Array::New(
buf->Buffer(), buf->ByteOffset() + 4, buf->ByteLength() - 4);
return deserialize->Call(context, v8::Undefined(isolate), 1, &payload)
.ToLocalChecked();
}
class IPCSerdesTest : public EnvironmentTestFixture {};
TEST_F(IPCSerdesTest, RoundTripsPrimitives) {
const v8::HandleScope handle_scope(isolate_);
const Argv argv;
Env test_env{handle_scope, argv};
v8::Local<v8::Context> context = isolate_->GetCurrentContext();
v8::Local<v8::Object> binding = GetIpcSerdesBinding(*test_env, context);
v8::Local<v8::Function> serialize =
GetFunction(context, binding, "serialize");
v8::Local<v8::Function> deserialize =
GetFunction(context, binding, "deserialize");
v8::Local<v8::Value> inputs[] = {
v8::Number::New(isolate_, 42),
v8::Number::New(isolate_, -3.14),
v8::String::NewFromUtf8Literal(isolate_, "hello world"),
v8::Boolean::New(isolate_, true),
v8::Null(isolate_),
};
for (v8::Local<v8::Value> input : inputs) {
v8::Local<v8::Value> result =
RoundTrip(context, serialize, deserialize, input);
EXPECT_TRUE(result->StrictEquals(input));
}
}
TEST_F(IPCSerdesTest, RoundTripsObject) {
const v8::HandleScope handle_scope(isolate_);
const Argv argv;
Env test_env{handle_scope, argv};
v8::Local<v8::Context> context = isolate_->GetCurrentContext();
v8::Local<v8::Object> binding = GetIpcSerdesBinding(*test_env, context);
v8::Local<v8::Function> serialize =
GetFunction(context, binding, "serialize");
v8::Local<v8::Function> deserialize =
GetFunction(context, binding, "deserialize");
v8::Local<v8::Object> input = v8::Object::New(isolate_);
input
->Set(context,
v8::String::NewFromUtf8Literal(isolate_, "foo"),
v8::Number::New(isolate_, 42))
.Check();
input
->Set(context,
v8::String::NewFromUtf8Literal(isolate_, "bar"),
v8::String::NewFromUtf8Literal(isolate_, "baz"))
.Check();
v8::Local<v8::Value> result =
RoundTrip(context, serialize, deserialize, input);
ASSERT_TRUE(result->IsObject());
v8::Local<v8::Object> obj = result.As<v8::Object>();
v8::Local<v8::Value> foo =
obj->Get(context, v8::String::NewFromUtf8Literal(isolate_, "foo"))
.ToLocalChecked();
EXPECT_EQ(foo->Int32Value(context).FromJust(), 42);
v8::Local<v8::Value> bar =
obj->Get(context, v8::String::NewFromUtf8Literal(isolate_, "bar"))
.ToLocalChecked();
v8::String::Utf8Value bar_utf8(isolate_, bar);
EXPECT_STREQ(*bar_utf8, "baz");
}
TEST_F(IPCSerdesTest, RoundTripsTypedArray) {
const v8::HandleScope handle_scope(isolate_);
const Argv argv;
Env test_env{handle_scope, argv};
v8::Local<v8::Context> context = isolate_->GetCurrentContext();
v8::Local<v8::Object> binding = GetIpcSerdesBinding(*test_env, context);
v8::Local<v8::Function> serialize =
GetFunction(context, binding, "serialize");
v8::Local<v8::Function> deserialize =
GetFunction(context, binding, "deserialize");
v8::Local<v8::ArrayBuffer> ab = v8::ArrayBuffer::New(isolate_, 4);
uint8_t* data = static_cast<uint8_t*>(ab->Data());
for (uint8_t i = 0; i < 4; i++) data[i] = i + 1;
v8::Local<v8::Uint8Array> input = v8::Uint8Array::New(ab, 0, 4);
v8::Local<v8::Value> result =
RoundTrip(context, serialize, deserialize, input);
ASSERT_TRUE(result->IsUint8Array());
v8::Local<v8::Uint8Array> out = result.As<v8::Uint8Array>();
ASSERT_EQ(out->ByteLength(), static_cast<size_t>(4));
node::ArrayBufferViewContents<uint8_t> out_bytes(result);
for (uint8_t i = 0; i < 4; i++) {
EXPECT_EQ(out_bytes.data()[i], i + 1);
}
// A plain Uint8Array must not come back as a Node Buffer.
Environment* env = *test_env;
EXPECT_FALSE(
out->GetPrototypeV2()->StrictEquals(env->buffer_prototype_object()));
}
TEST_F(IPCSerdesTest, BufferRoundTripsAsBuffer) {
const v8::HandleScope handle_scope(isolate_);
const Argv argv;
Env test_env{handle_scope, argv};
v8::Local<v8::Context> context = isolate_->GetCurrentContext();
v8::Local<v8::Object> binding = GetIpcSerdesBinding(*test_env, context);
v8::Local<v8::Function> serialize =
GetFunction(context, binding, "serialize");
v8::Local<v8::Function> deserialize =
GetFunction(context, binding, "deserialize");
v8::Local<v8::Object> input =
node::Buffer::Copy(isolate_, "Hello!", 6).ToLocalChecked();
v8::Local<v8::Value> result =
RoundTrip(context, serialize, deserialize, input);
ASSERT_TRUE(result->IsUint8Array());
v8::Local<v8::Object> out = result.As<v8::Object>();
// The key correctness property: a Buffer round-trips as a Buffer, not a
// plain Uint8Array.
Environment* env = *test_env;
EXPECT_TRUE(
out->GetPrototypeV2()->StrictEquals(env->buffer_prototype_object()));
EXPECT_EQ(node::Buffer::Length(out), static_cast<size_t>(6));
EXPECT_EQ(memcmp(node::Buffer::Data(out), "Hello!", 6), 0);
}
} // namespace