Skip to content

Commit 47c934e

Browse files
benchmark: conditionally use spawn with taskset for cpu pinning
This change enhances the benchmarking tool by conditionally using the, spawn method with taskset for CPU pinning, improving consistency of benchmark results across different environments. Fixes: #52233 PR-URL: #52253 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Raz Luvaton <rluvaton@gmail.com> Reviewed-By: Yagiz Nizipli <yagiz.nizipli@sentry.io>
1 parent ba07e4e commit 47c934e

File tree

4 files changed

+108
-10
lines changed

4 files changed

+108
-10
lines changed

benchmark/_cli.js

+21
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,24 @@ CLI.prototype.shouldSkip = function(scripts) {
125125

126126
return skip;
127127
};
128+
129+
/**
130+
* Extracts the CPU core setting from the CLI arguments.
131+
* @returns {string|null} The CPU core setting if found, otherwise null.
132+
*/
133+
CLI.prototype.getCpuCoreSetting = function() {
134+
const cpuCoreSetting = this.optional.set.find((s) => s.startsWith('CPUSET='));
135+
if (!cpuCoreSetting) return null;
136+
137+
const value = cpuCoreSetting.split('=')[1];
138+
// Validate the CPUSET value to match patterns like "0", "0-2", "0,1,2", "0,2-4,6" or "0,0,1-2"
139+
const isValid = /^(\d+(-\d+)?)(,\d+(-\d+)?)*$/.test(value);
140+
if (!isValid) {
141+
throw new Error(`
142+
Invalid CPUSET format: "${value}". Please use a single core number (e.g., "0"),
143+
a range of cores (e.g., "0-3"), or a list of cores/ranges
144+
(e.g., "0,2,4" or "0-2,4").\n\n${this.usage}
145+
`);
146+
}
147+
return value;
148+
};

benchmark/compare.js

+25-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use strict';
22

3-
const { fork } = require('child_process');
3+
const { spawn, fork } = require('node:child_process');
44
const { inspect } = require('util');
55
const path = require('path');
66
const CLI = require('./_cli.js');
@@ -24,6 +24,12 @@ const cli = new CLI(`usage: ./node compare.js [options] [--] <category> ...
2424
repeated)
2525
--set variable=value set benchmark variable (can be repeated)
2626
--no-progress don't show benchmark progress indicator
27+
28+
Examples:
29+
--set CPUSET=0 Runs benchmarks on CPU core 0.
30+
--set CPUSET=0-2 Specifies that benchmarks should run on CPU cores 0 to 2.
31+
32+
Note: The CPUSET format should match the specifications of the 'taskset' command
2733
`, { arrayArgs: ['set', 'filter', 'exclude'], boolArgs: ['no-progress'] });
2834

2935
if (!cli.optional.new || !cli.optional.old) {
@@ -69,10 +75,24 @@ if (showProgress) {
6975

7076
(function recursive(i) {
7177
const job = queue[i];
72-
73-
const child = fork(path.resolve(__dirname, job.filename), cli.optional.set, {
74-
execPath: cli.optional[job.binary],
75-
});
78+
const resolvedPath = path.resolve(__dirname, job.filename);
79+
80+
const cpuCore = cli.getCpuCoreSetting();
81+
let child;
82+
if (cpuCore !== null) {
83+
const spawnArgs = ['-c', cpuCore, cli.optional[job.binary], resolvedPath, ...cli.optional.set];
84+
child = spawn('taskset', spawnArgs, {
85+
env: process.env,
86+
stdio: ['inherit', 'pipe', 'pipe'],
87+
});
88+
89+
child.stdout.pipe(process.stdout);
90+
child.stderr.pipe(process.stderr);
91+
} else {
92+
child = fork(resolvedPath, cli.optional.set, {
93+
execPath: cli.optional[job.binary],
94+
});
95+
}
7696

7797
child.on('message', (data) => {
7898
if (data.type === 'report') {

benchmark/run.js

+26-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use strict';
22

33
const path = require('path');
4-
const fork = require('child_process').fork;
4+
const { spawn, fork } = require('node:child_process');
55
const CLI = require('./_cli.js');
66

77
const cli = new CLI(`usage: ./node run.js [options] [--] <category> ...
@@ -17,7 +17,14 @@ const cli = new CLI(`usage: ./node run.js [options] [--] <category> ...
1717
test only run a single configuration from the options
1818
matrix
1919
all each benchmark category is run one after the other
20+
21+
Examples:
22+
--set CPUSET=0 Runs benchmarks on CPU core 0.
23+
--set CPUSET=0-2 Specifies that benchmarks should run on CPU cores 0 to 2.
24+
25+
Note: The CPUSET format should match the specifications of the 'taskset' command on your system.
2026
`, { arrayArgs: ['set', 'filter', 'exclude'] });
27+
2128
const benchmarks = cli.benchmarks();
2229

2330
if (benchmarks.length === 0) {
@@ -40,10 +47,24 @@ if (format === 'csv') {
4047

4148
(function recursive(i) {
4249
const filename = benchmarks[i];
43-
const child = fork(
44-
path.resolve(__dirname, filename),
45-
cli.test ? ['--test'] : cli.optional.set,
46-
);
50+
const scriptPath = path.resolve(__dirname, filename);
51+
52+
const args = cli.test ? ['--test'] : cli.optional.set;
53+
const cpuCore = cli.getCpuCoreSetting();
54+
let child;
55+
if (cpuCore !== null) {
56+
child = spawn('taskset', ['-c', cpuCore, 'node', scriptPath, ...args], {
57+
stdio: ['inherit', 'pipe', 'pipe'],
58+
});
59+
60+
child.stdout.pipe(process.stdout);
61+
child.stderr.pipe(process.stderr);
62+
} else {
63+
child = fork(
64+
scriptPath,
65+
args,
66+
);
67+
}
4768

4869
if (format !== 'csv') {
4970
console.log();

doc/contributing/writing-and-running-benchmarks.md

+36
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
* [Running benchmarks](#running-benchmarks)
1111
* [Running individual benchmarks](#running-individual-benchmarks)
1212
* [Running all benchmarks](#running-all-benchmarks)
13+
* [Specifying CPU Cores for Benchmarks with run.js](#specifying-cpu-cores-for-benchmarks-with-runjs)
1314
* [Filtering benchmarks](#filtering-benchmarks)
1415
* [Comparing Node.js versions](#comparing-nodejs-versions)
1516
* [Comparing parameters](#comparing-parameters)
@@ -163,6 +164,33 @@ It is possible to execute more groups by adding extra process arguments.
163164
node benchmark/run.js assert async_hooks
164165
```
165166

167+
#### Specifying CPU Cores for Benchmarks with run.js
168+
169+
When using `run.js` to execute a group of benchmarks,
170+
you can specify on which CPU cores the
171+
benchmarks should execute
172+
by using the `--set CPUSET=value` option.
173+
This controls the CPU core
174+
affinity for the benchmark process,
175+
potentially reducing
176+
interference from other processes and allowing
177+
for performance
178+
testing under specific hardware configurations.
179+
180+
The `CPUSET` option utilizes the `taskset` command's format
181+
for setting CPU affinity, where `value` can be a single core
182+
number or a range of cores.
183+
184+
Examples:
185+
186+
* `node benchmark/run.js --set CPUSET=0` ... runs benchmarks on CPU core 0.
187+
* `node benchmark/run.js --set CPUSET=0-2` ...
188+
specifies that benchmarks should run on CPU cores 0 to 2.
189+
190+
Note: This option is only applicable when using `run.js`.
191+
Ensure the `taskset` command is available on your system
192+
and the specified `CPUSET` format matches its requirements.
193+
166194
#### Filtering benchmarks
167195

168196
`benchmark/run.js` and `benchmark/compare.js` have `--filter pattern` and
@@ -288,8 +316,16 @@ module, you can use the `--filter` option:_
288316
--old ./old-node-binary old node binary (required)
289317
--runs 30 number of samples
290318
--filter pattern string to filter benchmark scripts
319+
--exclude pattern excludes scripts matching <pattern> (can be
320+
repeated)
291321
--set variable=value set benchmark variable (can be repeated)
292322
--no-progress don't show benchmark progress indicator
323+
324+
Examples:
325+
--set CPUSET=0 Runs benchmarks on CPU core 0.
326+
--set CPUSET=0-2 Specifies that benchmarks should run on CPU cores 0 to 2.
327+
328+
Note: The CPUSET format should match the specifications of the 'taskset' command
293329
```
294330

295331
For analyzing the benchmark results, use [node-benchmark-compare][] or the R

0 commit comments

Comments
 (0)