Skip to content

Commit

Permalink
benchmark: conditionally use spawn with taskset for cpu pinning
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
thisalihassan authored and marco-ippolito committed May 3, 2024
1 parent edecd46 commit 7508d48
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 10 deletions.
21 changes: 21 additions & 0 deletions benchmark/_cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,24 @@ CLI.prototype.shouldSkip = function(scripts) {

return skip;
};

/**
* Extracts the CPU core setting from the CLI arguments.
* @returns {string|null} The CPU core setting if found, otherwise null.
*/
CLI.prototype.getCpuCoreSetting = function() {
const cpuCoreSetting = this.optional.set.find((s) => s.startsWith('CPUSET='));
if (!cpuCoreSetting) return null;

const value = cpuCoreSetting.split('=')[1];
// Validate the CPUSET value to match patterns like "0", "0-2", "0,1,2", "0,2-4,6" or "0,0,1-2"
const isValid = /^(\d+(-\d+)?)(,\d+(-\d+)?)*$/.test(value);
if (!isValid) {
throw new Error(`
Invalid CPUSET format: "${value}". Please use a single core number (e.g., "0"),
a range of cores (e.g., "0-3"), or a list of cores/ranges
(e.g., "0,2,4" or "0-2,4").\n\n${this.usage}
`);
}
return value;
};
30 changes: 25 additions & 5 deletions benchmark/compare.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict';

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

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

(function recursive(i) {
const job = queue[i];

const child = fork(path.resolve(__dirname, job.filename), cli.optional.set, {
execPath: cli.optional[job.binary],
});
const resolvedPath = path.resolve(__dirname, job.filename);

const cpuCore = cli.getCpuCoreSetting();
let child;
if (cpuCore !== null) {
const spawnArgs = ['-c', cpuCore, cli.optional[job.binary], resolvedPath, ...cli.optional.set];
child = spawn('taskset', spawnArgs, {
env: process.env,
stdio: ['inherit', 'pipe', 'pipe'],
});

child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);
} else {
child = fork(resolvedPath, cli.optional.set, {
execPath: cli.optional[job.binary],
});
}

child.on('message', (data) => {
if (data.type === 'report') {
Expand Down
31 changes: 26 additions & 5 deletions benchmark/run.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict';

const path = require('path');
const fork = require('child_process').fork;
const { spawn, fork } = require('node:child_process');
const CLI = require('./_cli.js');

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

const benchmarks = cli.benchmarks();

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

(function recursive(i) {
const filename = benchmarks[i];
const child = fork(
path.resolve(__dirname, filename),
cli.test ? ['--test'] : cli.optional.set,
);
const scriptPath = path.resolve(__dirname, filename);

const args = cli.test ? ['--test'] : cli.optional.set;
const cpuCore = cli.getCpuCoreSetting();
let child;
if (cpuCore !== null) {
child = spawn('taskset', ['-c', cpuCore, 'node', scriptPath, ...args], {
stdio: ['inherit', 'pipe', 'pipe'],
});

child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);
} else {
child = fork(
scriptPath,
args,
);
}

if (format !== 'csv') {
console.log();
Expand Down
36 changes: 36 additions & 0 deletions doc/contributing/writing-and-running-benchmarks.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* [Running benchmarks](#running-benchmarks)
* [Running individual benchmarks](#running-individual-benchmarks)
* [Running all benchmarks](#running-all-benchmarks)
* [Specifying CPU Cores for Benchmarks with run.js](#specifying-cpu-cores-for-benchmarks-with-runjs)
* [Filtering benchmarks](#filtering-benchmarks)
* [Comparing Node.js versions](#comparing-nodejs-versions)
* [Comparing parameters](#comparing-parameters)
Expand Down Expand Up @@ -163,6 +164,33 @@ It is possible to execute more groups by adding extra process arguments.
node benchmark/run.js assert async_hooks
```

#### Specifying CPU Cores for Benchmarks with run.js

When using `run.js` to execute a group of benchmarks,
you can specify on which CPU cores the
benchmarks should execute
by using the `--set CPUSET=value` option.
This controls the CPU core
affinity for the benchmark process,
potentially reducing
interference from other processes and allowing
for performance
testing under specific hardware configurations.

The `CPUSET` option utilizes the `taskset` command's format
for setting CPU affinity, where `value` can be a single core
number or a range of cores.

Examples:

* `node benchmark/run.js --set CPUSET=0` ... runs benchmarks on CPU core 0.
* `node benchmark/run.js --set CPUSET=0-2` ...
specifies that benchmarks should run on CPU cores 0 to 2.

Note: This option is only applicable when using `run.js`.
Ensure the `taskset` command is available on your system
and the specified `CPUSET` format matches its requirements.

#### Filtering benchmarks

`benchmark/run.js` and `benchmark/compare.js` have `--filter pattern` and
Expand Down Expand Up @@ -288,8 +316,16 @@ module, you can use the `--filter` option:_
--old ./old-node-binary old node binary (required)
--runs 30 number of samples
--filter pattern string to filter benchmark scripts
--exclude pattern excludes scripts matching <pattern> (can be
repeated)
--set variable=value set benchmark variable (can be repeated)
--no-progress don't show benchmark progress indicator

Examples:
--set CPUSET=0 Runs benchmarks on CPU core 0.
--set CPUSET=0-2 Specifies that benchmarks should run on CPU cores 0 to 2.

Note: The CPUSET format should match the specifications of the 'taskset' command
```

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

0 comments on commit 7508d48

Please sign in to comment.