Repository: nodejs/node
Pull request: cli: support `${pid}` placeholder in --cpu-prof-name

This patch adds support for runtime substitution of the ${pid} placeholder
in the --cpu-prof-name option. This is helpful when using cluster/forked
processes to avoid profile file name collisions.

Previously, patterns like ${pid} were not substituted. Now, ${pid} will
be replaced with the current process ID.

Example:
  node --cpu-prof --cpu-prof-name 'CPU.${pid}.cpuprofile' index.js
  # Generates: CPU.12345.cpuprofile

Also updated the CLI documentation accordingly.

Fixes: #57418

<!--
Before submitting a pull request, please read:

- the CONTRIBUTING guide at https://github.com/nodejs/node/blob/HEAD/CONTRIBUTING.md
- the commit message formatting guidelines at
  https://github.com/nodejs/node/blob/HEAD/doc/contributing/pull-requests.md#commit-message-guidelines

For code changes:
1. Include tests for any bug fixes or new features.
2. Update documentation if relevant.
3. Ensure that `make -j4 test` (UNIX), or `vcbuild test` (Windows) passes.

If you believe this PR should be highlighted in the Node.js CHANGELOG
please add the `notable-change` label.

Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.
-->


Changed files:
filename: doc/api/cli.md
patch: @@ -538,13 +538,16 @@ $ ls *.cpuprofile
 CPU.20190409.202950.15293.0.0.cpuprofile
 ```
 
-If `--cpu-prof-name` is specified, the provided value will be used as-is; patterns such as
-`${hhmmss}` or `${pid}` are not supported.
+If `--cpu-prof-name` is specified, the provided value is used as a template
+for the file name. The following placeholder is supported and will be
+substituted at runtime:
+
+* `${pid}` — the current process ID
 
 ```console
 $ node --cpu-prof --cpu-prof-name 'CPU.${pid}.cpuprofile' index.js
 $ ls *.cpuprofile
-'CPU.${pid}.cpuprofile'
+CPU.15293.cpuprofile
 ```
 
 ### `--cpu-prof-dir`

filename: src/inspector_profiler.cc
patch: @@ -8,6 +8,7 @@
 #include "node_file.h"
 #include "node_internals.h"
 #include "util-inl.h"
+#include "uv.h"
 #include "v8-inspector.h"
 
 #include <cinttypes>
@@ -465,6 +466,27 @@ static void EndStartedProfilers(Environment* env) {
   }
 }
 
+static std::string ReplacePlaceholders(const std::string& pattern) {
+  std::string result = pattern;
+
+  static const std::unordered_map<std::string, std::function<std::string()>>
+      kPlaceholderMap = {
+          {"${pid}", []() { return std::to_string(uv_os_getpid()); }},
+          // TODO(haramj): Add more placeholders as needed.
+      };
+
+  for (const auto& [placeholder, getter] : kPlaceholderMap) {
+    size_t pos = 0;
+    while ((pos = result.find(placeholder, pos)) != std::string::npos) {
+      const std::string value = getter();
+      result.replace(pos, placeholder.length(), value);
+      pos += value.length();
+    }
+  }
+
+  return result;
+}
+
 void StartProfilers(Environment* env) {
   AtExit(env, [](void* env) {
     EndStartedProfilers(static_cast<Environment*>(env));
@@ -486,7 +508,9 @@ void StartProfilers(Environment* env) {
       DiagnosticFilename filename(env, "CPU", "cpuprofile");
       env->set_cpu_prof_name(*filename);
     } else {
-      env->set_cpu_prof_name(env->options()->cpu_prof_name);
+      std::string resolved_name =
+          ReplacePlaceholders(env->options()->cpu_prof_name);
+      env->set_cpu_prof_name(resolved_name);
     }
     CHECK_NULL(env->cpu_profiler_connection());
     env->set_cpu_profiler_connection(

filename: test/sequential/test-cpu-prof-name.js
patch: @@ -8,6 +8,8 @@ const fixtures = require('../common/fixtures');
 common.skipIfInspectorDisabled();
 
 const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
 const { spawnSync } = require('child_process');
 
 const tmpdir = require('../common/tmpdir');
@@ -41,3 +43,36 @@ const {
   assert.deepStrictEqual(profiles, [file]);
   verifyFrames(output, file, 'fibonacci.js');
 }
+
+// --cpu-prof-name with ${pid} placeholder
+{
+  tmpdir.refresh();
+  // eslint-disable-next-line no-template-curly-in-string
+  const profName = 'CPU.${pid}.cpuprofile';
+  const dir = tmpdir.path;
+
+  const output = spawnSync(process.execPath, [
+    '--cpu-prof',
+    '--cpu-prof-interval',
+    kCpuProfInterval,
+    '--cpu-prof-name',
+    profName,
+    fixtures.path('workload', 'fibonacci.js'),
+  ], {
+    cwd: dir,
+    env
+  });
+
+  if (output.status !== 0) {
+    console.error(output.stderr.toString());
+  }
+
+  assert.strictEqual(output.status, 0);
+
+  const expectedFile = path.join(dir, `CPU.${output.pid}.cpuprofile`);
+  assert.ok(fs.existsSync(expectedFile), `Expected file ${expectedFile} not found.`);
+
+  verifyFrames(output, expectedFile, 'fibonacci.js');
+
+  fs.unlinkSync(expectedFile);
+}