Skip to content

Commit 3fd0e11

Browse files
authored
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: #63929 Reviewed-By: Filip Skokan <panva.ip@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent b03e6e0 commit 3fd0e11

3 files changed

Lines changed: 129 additions & 0 deletions

File tree

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

0 commit comments

Comments
 (0)