mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
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:
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();
|
||||
}
|
||||
Reference in new issue
Block a user