Skip to content

child_process: frame advanced IPC messages natively#64072

Open
anonrig wants to merge 1 commit into
nodejs:mainfrom
anonrig:child-process-native-ipc-framing
Open

child_process: frame advanced IPC messages natively#64072
anonrig wants to merge 1 commit into
nodejs:mainfrom
anonrig:child-process-native-ipc-framing

Conversation

@anonrig

@anonrig anonrig commented Jun 22, 2026

Copy link
Copy Markdown
Member

Summary

Move the advanced child_process IPC read-path framing into C++,
continuing the child_process JS→C++ migration.

The advanced codec previously reframed every read in JavaScript
(parseChannelMessages in lib/internal/child_process/serialization.js):
scanning the 4-byte big-endian length prefix, accumulating partial frames in
an array, and Buffer.concat-ing messages that span reads, then crossing
into the ipc_serdes binding once per message to deserialize.

This adds a native per-channel IPCChannelFramer (a BaseObject on the
ipc_serdes binding) that owns the cross-read accumulation buffer in C++ and
returns an array of complete, deserialized messages per read, so JavaScript
no longer reframes the byte stream. Messages contained entirely within a read
are deserialized in place and let host objects borrow the read buffer (zero
copy); only frames that span reads are copied (matching the previous
Buffer.concat behavior).

Invariants preserved

  • Wire format is byte-identical (4-byte BE length prefix + V8 payload with
    the custom host-object tags).
  • Handle passing is untouched: the per-read pendingHandle handoff and
    NODE_HANDLE delivery stay in setupChannel.
  • Public events and their timing are unchanged.

json stays in JavaScript (deliberate)

I implemented and benchmarked native json framing as well, but it
regressed the json read path by up to ~40% on large messages: its
StringDecoder + split('\n') pipeline is already copy-free (V8 rope
concatenation and O(1) substrings), whereas reframing in C++ adds a buffer
copy plus a per-message String::NewFromUtf8. Only advanced is framed
natively; the json codec is left unchanged.

Benchmarks

benchmark/child_process/child-process-ipc-roundtrip (advanced, msgs/sec;
interleaved A/B vs. the pre-change baseline, 8 reps × dur=5s):

payload before after delta
1 KiB 559,040 626,255 +12.0% (t=5.3)
4 KiB 311,284 330,153 +6.1% (t=2.3)
16 KiB 104,077 125,181 +20.3%
64 KiB 31,921 35,739 +12.0%

json round-trip and read throughput are unchanged (a json control row in the
same harness lands within the noise floor).

Tests

  • New cctests in test/cctest/test_node_ipc_serdes.cc: single-message
    round-trip, several messages delivered in one read, and a frame split across
    two reads (including a split length header).
  • test/parallel/test-child-process-*, test/parallel/test-cluster-*, and
    test/sequential/*child* pass locally.

@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. child_process Issues and PRs related to the child_process subsystem. needs-ci PRs that need a full CI run. typings labels Jun 22, 2026
The `advanced` IPC read path framed messages in JavaScript
(parseChannelMessages in lib/internal/child_process/serialization.js):
every read scanned the 4-byte big-endian length prefix, accumulated
partial frames in an array, and called Buffer.concat to reassemble
messages that spanned reads, then crossed into the ipc_serdes binding
once per message to deserialize.

Move framing into a native per-channel IPCChannelFramer (a BaseObject on
the ipc_serdes binding). It owns the cross-read accumulation buffer in
C++ and returns an array of complete, deserialized messages per read, so
JavaScript no longer reframes the byte stream. Messages that lie
entirely within a read are deserialized in place and let host objects
borrow the read buffer (zero copy); only frames that span reads are
copied, matching the previous Buffer.concat behavior. The wire format
(4-byte BE length prefix + V8 payload with custom host-object tags) is
preserved byte-for-byte, and handle passing is untouched: the per-read
pendingHandle handoff and NODE_HANDLE delivery stay in setupChannel.

The `json` read path is deliberately left in JavaScript. Its
StringDecoder + split('\n') pipeline avoids copies (V8 rope
concatenation and O(1) substrings); reframing it in C++ regressed the
json read path by up to ~40% on large messages in benchmarks, so only
`advanced` is framed natively.

A cctest (test/cctest/test_node_ipc_serdes.cc) exercises the framer
directly: single-message round-trips, several messages delivered in one
read, and a frame split across two reads (including a split length
header).

Round-trip throughput
(benchmark/child_process/child-process-ipc-roundtrip, advanced;
interleaved A/B vs. the pre-change baseline, 8 reps x dur=5s):

  payload   before     after      change
  1 KiB     559,040    626,255    +12.0% (t=5.3)
  4 KiB     311,284    330,153     +6.1% (t=2.3)
  16 KiB    104,077    125,181    +20.3%
  64 KiB     31,921     35,739    +12.0%

json round-trip and read throughput are unchanged (within noise).

Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
@anonrig anonrig force-pushed the child-process-native-ipc-framing branch from 0ab8538 to fee0262 Compare June 22, 2026 17:59
@anonrig

anonrig commented Jun 22, 2026

Copy link
Copy Markdown
Member Author

@anonrig

anonrig commented Jun 22, 2026

Copy link
Copy Markdown
Member Author
Config old (ops/s) new (ops/s) Δ sig
advanced len=64 467,991 488,482 +4.4% ***
advanced len=256 399,774 411,011 +2.8% ***
advanced len=1024 321,322 336,254 +4.7% ***
advanced len=4096 210,270 225,204 +7.1% ***
advanced len=16384 91,388 104,764 +14.6% ***
advanced len=65536 21,868 21,574 -1.3% ***
json len=64 692,902 689,566 -0.5%
json len=256 519,877 513,564 -1.2% ***
json len=1024 334,806 332,229 -0.8% **
json len=4096 161,677 160,458 -0.7% **
json len=16384 57,085 56,531 -1.0% ***
json len=65536 13,297 13,161 -1.0%

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c++ Issues and PRs that require attention from people who are familiar with C++. child_process Issues and PRs related to the child_process subsystem. needs-ci PRs that need a full CI run. typings

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants