From 47c934e464756952719927529c2d28a92a58d701 Mon Sep 17 00:00:00 2001 From: Ali Hassan <24819103+thisalihassan@users.noreply.github.com> Date: Sun, 7 Apr 2024 03:43:53 +0500 Subject: [PATCH] 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: https://github.com/nodejs/node/issues/52233 PR-URL: https://github.com/nodejs/node/pull/52253 Reviewed-By: Matteo Collina Reviewed-By: Raz Luvaton Reviewed-By: Yagiz Nizipli --- benchmark/_cli.js | 21 +++++++++++ benchmark/compare.js | 30 +++++++++++++--- benchmark/run.js | 31 +++++++++++++--- .../writing-and-running-benchmarks.md | 36 +++++++++++++++++++ 4 files changed, 108 insertions(+), 10 deletions(-) diff --git a/benchmark/_cli.js b/benchmark/_cli.js index eb6c4add9799a4..33ba2e0963f2fe 100644 --- a/benchmark/_cli.js +++ b/benchmark/_cli.js @@ -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; +}; diff --git a/benchmark/compare.js b/benchmark/compare.js index 503901f607ef02..1efff9e85c072f 100644 --- a/benchmark/compare.js +++ b/benchmark/compare.js @@ -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'); @@ -24,6 +24,12 @@ const cli = new CLI(`usage: ./node compare.js [options] [--] ... 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) { @@ -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') { diff --git a/benchmark/run.js b/benchmark/run.js index 0b63fb930bb000..11f95d8e71f035 100644 --- a/benchmark/run.js +++ b/benchmark/run.js @@ -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] [--] ... @@ -17,7 +17,14 @@ const cli = new CLI(`usage: ./node run.js [options] [--] ... 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) { @@ -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(); diff --git a/doc/contributing/writing-and-running-benchmarks.md b/doc/contributing/writing-and-running-benchmarks.md index 2c8d5eb3ed237b..f70ff965a8d7d1 100644 --- a/doc/contributing/writing-and-running-benchmarks.md +++ b/doc/contributing/writing-and-running-benchmarks.md @@ -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) @@ -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 @@ -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 (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