benchmark: add child_process async path baselines

Add micro-benchmarks that isolate the hot paths targeted by the
JavaScript-to-C++ migration of child_process:

- child-process-spawn-options.js scales the env vars and args that
  ProcessWrap::Spawn must marshal across the JS/C++ boundary.
- child-process-ipc-roundtrip.js measures IPC throughput for both the
  json and advanced serializers across a range of payload sizes.
- child-process-exec-maxbuffer.js measures stdout accumulation and
  maxBuffer handling in execFile().

These establish the baseline that later migration PRs are compared
against. There is no runtime behavior change.

Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
PR-URL: https://github.com/nodejs/node/pull/63929
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
This commit is contained in:
Yagiz Nizipli authored and GitHub committed 2026-06-17 16:08:56 +00:00
1 parent b03e6e0997
commit 3fd0e1187d
3 files changed
+129

No files matched your search

@@ -0,0 +1,39 @@
'use strict';
const common = require('../common.js');
const { execFile } = require('child_process');
// Isolates stdout accumulation + maxBuffer handling in execFile(). The child
// writes `chunks` blocks of 64 KiB; the parent accumulates them through the
// native pipe read path and the JS buffering in lib/child_process.js until the
// process exits and the result buffer is handed to the callback.
const bench = common.createBenchmark(main, {
// Number of 64 KiB blocks written by the child: 1 MiB, 16 MiB, 64 MiB.
chunks: [16, 256, 1024],
n: [10],
});
function main({ n, chunks }) {
const script =
'const b = Buffer.alloc(65536, 0x61);' +
`for (let i = 0; i < ${chunks}; i++) process.stdout.write(b);`;
const args = ['-e', script];
const options = {
maxBuffer: chunks * 65536 + 65536,
encoding: 'buffer',
};
let left = n;
const run = () => {
execFile(process.execPath, args, options, (err) => {
if (err)
throw err;
if (--left === 0)
return bench.end(n);
run();
});
};
bench.start();
run();
}
@@ -0,0 +1,47 @@
'use strict';
if (process.argv[2] === 'child') {
// Echo every message straight back to the parent.
process.on('message', (msg) => {
process.send(msg);
});
} else {
const common = require('../common.js');
const bench = common.createBenchmark(main, {
len: [64, 256, 1024, 4096, 16384, 65536],
serialization: ['json', 'advanced'],
dur: [5],
});
const { spawn } = require('child_process');
function main({ dur, len, serialization }) {
const msg = { payload: '.'.repeat(len) };
const options = {
stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
serialization,
};
const child = spawn(process.argv[0],
[process.argv[1], 'child'], options);
let messages = 0;
let finished = false;
child.on('message', () => {
messages++;
// Keep one round-trip in flight per completed one so both the serialize
// (write) and deserialize (read) paths stay saturated on both ends.
if (!finished)
child.send(msg);
});
bench.start();
// Prime a window of in-flight messages so the IPC channel never drains.
for (let i = 0; i < 256; i++)
child.send(msg);
setTimeout(() => {
finished = true;
bench.end(messages);
child.kill();
}, dur * 1000);
}
}
@@ -0,0 +1,43 @@
'use strict';
const common = require('../common.js');
const { spawn } = require('child_process');
// Isolates the cost of marshaling spawn() options across the JS -> C++ boundary
// (ProcessWrap::Spawn). A trivial, fast-exiting child is spawned repeatedly
// while scaling the number of environment pairs and arguments that have to be
// converted, so the per-spawn option-handling overhead is the dominant cost.
const isWindows = process.platform === 'win32';
const command = isWindows ? 'cmd' : 'true';
const baseArgs = isWindows ? ['/d', '/s', '/c', 'exit'] : [];
const bench = common.createBenchmark(main, {
n: [1000],
envc: [0, 64, 256, 1024],
argc: [0, 8, 64],
});
function main({ n, envc, argc }) {
const env = { ...process.env };
for (let i = 0; i < envc; i++)
env[`NODE_BENCH_VAR_${i}`] = `value_${i}`;
const args = baseArgs.slice();
for (let i = 0; i < argc; i++)
args.push(`arg_${i}`);
const options = { env, stdio: ['ignore', 'ignore', 'ignore'] };
let left = n;
const go = () => {
if (--left < 0)
return bench.end(n);
const child = spawn(command, args, options);
// The exit code is intentionally ignored: the child only exercises the
// option-marshaling path, it is not expected to do any useful work.
child.on('exit', go);
};
bench.start();
go();
}