Commit 71c63912a71 for nodejs

commit 71c63912a716fbe643fbdab42a719dc4de781821
Author: Chengzhong Wu <legendecas@gmail.com>
Date:   Sat Sep 26 13:19:09 2026 -0400

    build: add clang support for pgo on macOS/Linux

    Signed-off-by: Chengzhong Wu <legendecas@gmail.com>
    PR-URL: https://github.com/nodejs/node/pull/66136
    Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>

diff --git a/.gitignore b/.gitignore
index 1899fdc0900..2a878492247 100644
--- a/.gitignore
+++ b/.gitignore
@@ -103,6 +103,11 @@ _UpgradeReport_Files/
 tools/*/*.i
 tools/*/*.i.tmp

+# === Rules for pgo artifacts ===
+*.profdata
+*.profraw
+*.gcda
+
 # === Rules for release artifacts ===
 /*.tar.*
 /*.pkg
diff --git a/common.gypi b/common.gypi
index 3e37e440b86..ce50adc016e 100644
--- a/common.gypi
+++ b/common.gypi
@@ -12,6 +12,7 @@
     'msvs_multi_core_compile': '0',   # we do enable multicore compiles, but not using the V8 way
     'enable_pgo_generate%': '0',
     'enable_pgo_use%': '0',
+    'pgo_profile%': '',
     'clang_profile_lib%': '',
     'python%': 'python',
     'emulator%': [],
@@ -193,6 +194,7 @@
             }],
             ['clang==1', {
               'lto': ' -flto ', # Clang
+              'pgo_use': '-fprofile-use=<(pgo_profile)',
             }, {
               'lto': ' -flto=4 -ffat-lto-objects ', # GCC
             }],
@@ -247,6 +249,34 @@
               },],
             ],
           },],
+          ['OS=="mac"', {
+            'conditions': [
+              ['enable_pgo_generate=="true"', {
+                'xcode_settings': {
+                  'OTHER_CFLAGS': ['<(pgo_generate)'],
+                },
+                'target_conditions': [
+                  ['_type!="static_library"', {
+                    'xcode_settings': {
+                      'OTHER_LDFLAGS': ['<(pgo_generate)'],
+                    },
+                  }],
+                ],
+              }],
+              ['enable_pgo_use=="true"', {
+                'xcode_settings': {
+                  'OTHER_CFLAGS': ['<(pgo_use)'],
+                },
+                'target_conditions': [
+                  ['_type!="static_library"', {
+                    'xcode_settings': {
+                      'OTHER_LDFLAGS': ['<(pgo_use)'],
+                    },
+                  }],
+                ],
+              }],
+            ],
+          }],
           ['OS=="win"', {
             'conditions': [
               ['enable_lto=="true"', {
diff --git a/configure.py b/configure.py
index 4bf8e2e39ee..e769236cebe 100755
--- a/configure.py
+++ b/configure.py
@@ -208,14 +208,15 @@ parser.add_argument("--enable-pgo-generate",
     dest="enable_pgo_generate",
     default=None,
     help="Enable profiling with pgo of a binary. This feature is only available "
-         "on linux with gcc and g++ 5.4.1 or newer and on windows.")
+         "on linux with GCC or Clang, on macOS with Clang, and on windows.")

 parser.add_argument("--enable-pgo-use",
     action="store_true",
     dest="enable_pgo_use",
     default=None,
     help="Enable use of the profile generated with --enable-pgo-generate. This "
-         "feature is only available on linux with gcc and g++ 5.4.1 or newer and on windows.")
+         "feature is only available on linux with GCC or Clang, on macOS "
+         "with Clang, and on windows.")

 parser.add_argument("--enable-lto",
     action="store_true",
@@ -2004,18 +2005,9 @@ def configure_node(o):
   else:
     o['variables']['node_enable_v8_vtunejit'] = 'false'

-  if (flavor != 'linux' and flavor != 'win') and (options.enable_pgo_generate or options.enable_pgo_use):
+  if flavor not in ('linux', 'mac', 'win') and (options.enable_pgo_generate or options.enable_pgo_use):
     raise Exception(
-      'The pgo option is supported only on linux and windows.')
-
-  if flavor == 'linux':
-    if options.enable_pgo_generate or options.enable_pgo_use:
-      version_checked = (5, 4, 1)
-      if not gcc_version_ge(version_checked):
-        version_checked_str = ".".join(map(str, version_checked))
-        raise Exception(
-          'The options --enable-pgo-generate and --enable-pgo-use '
-          f'are supported for gcc and gxx {version_checked_str} or newer only.')
+      'The pgo option is supported only on linux, macOS, and windows.')

   if options.enable_pgo_generate and options.enable_pgo_use:
     raise Exception(
@@ -2024,6 +2016,24 @@ def configure_node(o):
       '--enable-pgo-generate first, profile node, and then recompile '
       'with --enable-pgo-use')

+  if flavor in ('linux', 'mac'):
+    if options.enable_pgo_generate or options.enable_pgo_use:
+      clang_compilers = [try_check_compiler(compiler, language)[1]
+                         for compiler, language in ((CC, 'c'), (CXX, 'c++'))]
+      if all(clang_compilers):
+        profile = os.path.abspath('node.profdata')
+        if options.enable_pgo_use and not os.path.isfile(profile):
+          raise Exception(
+            f'PGO profile not found: {profile}. Run llvm-profdata merge first.')
+        o['variables']['pgo_profile'] = profile
+      elif flavor == 'mac' or any(clang_compilers):
+        raise Exception('PGO requires both CC and CXX to use Clang on macOS '
+                        'or the same compiler family on linux.')
+      elif not gcc_version_ge((5, 4, 1)):
+        raise Exception(
+          'The options --enable-pgo-generate and --enable-pgo-use '
+          'require gcc and gxx 5.4.1 or newer.')
+
   o['variables']['enable_pgo_generate'] = b(options.enable_pgo_generate)
   o['variables']['enable_pgo_use']      = b(options.enable_pgo_use)

diff --git a/tools/pgo/README.md b/tools/pgo/README.md
index 234f20e638a..95653521792 100644
--- a/tools/pgo/README.md
+++ b/tools/pgo/README.md
@@ -15,11 +15,11 @@ The process has three phases:

 ## Platform Support

-| Platform | Supported toolchains | Driver                    |
-| -------- | -------------------- | ------------------------- |
-| Windows  | Clang-CL             | `vcbuild.bat` + `pgo.ps1` |
-| Linux    | GCC                  | `configure` + `make`      |
-| macOS    | —                    | —                         |
+| Platform | Supported toolchains | Driver                                |
+| -------- | -------------------- | ------------------------------------- |
+| Windows  | Clang-CL             | `vcbuild.bat` + `pgo.py`                |
+| Linux    | GCC, Clang           | `configure` + `make`, `pgo.py` for Clang |
+| macOS    | Clang                | `configure` + `make` + `pgo.py`         |

 The two supported flows differ in how profile data is collected. Clang writes
 one `.profraw` file per process, which must be merged into a single
@@ -27,8 +27,6 @@ one `.profraw` file per process, which must be merged into a single
 into `.gcda` files next to each object file as each process exits, so there is
 no merge step.

-Clang on Linux and macOS are not supported yet.
-
 ## Quick Start: Windows

 From a VS Developer Command Prompt, at the repo root:
@@ -38,25 +36,25 @@ From a VS Developer Command Prompt, at the repo root:
 vcbuild.bat pgo-generate

 # Step 2: Run workloads to collect profile data
-powershell -ExecutionPolicy Bypass -File .\tools\pgo\pgo.ps1
+python tools\pgo\pgo.py

 # Step 3: Build the optimized binary
 vcbuild.bat pgo-use
 ```

-`pgo.ps1` expects the instrumented binary at `Release\node.exe` (produced by
+`pgo.py` expects the instrumented binary at `Release\node.exe` (produced by
 step 1) and writes `node.profdata` to the repo root (consumed by step 3).

-The script is unsigned, so the default execution policy refuses to run it
-without `-ExecutionPolicy Bypass`. Use `pwsh` in place of `powershell` on
-PowerShell 7.
+The script finds `llvm-profdata` in the Visual Studio LLVM toolset, then
+`PATH`. Set `LLVM_PROFDATA` to the matching tool when using a different
+Clang installation.

 ```powershell
 # Optionally set a longer training duration (default: 15s per script)
-powershell -ExecutionPolicy Bypass -File .\tools\pgo\pgo.ps1 -Duration 30
+python tools\pgo\pgo.py --duration=30
 ```

-## Quick Start: Linux
+## Quick Start: Linux with GCC

 ```bash
 # Step 1: Build the instrumented binary
@@ -88,6 +86,42 @@ updates from the worker threads and the libuv thread pool race with each
 other, and GCC treats the resulting inconsistent profile as an error unless
 told to smooth it out.

+## Quick Start: Linux and macOS with Clang
+
+From the repo root:
+
+```bash
+# Step 1: Build the instrumented binary
+./configure --ninja --enable-pgo-generate
+make
+
+# Step 2: Run workloads to collect profile data
+python3 tools/pgo/pgo.py
+
+# Step 3: Build the optimized binary
+./configure --ninja --enable-pgo-use
+make
+```
+
+`pgo.py` expects the instrumented binary at `out/Release/node` (produced by
+step 1) and writes `node.profdata` to the repo root (consumed by step 3).
+
+The script finds `llvm-profdata` through `xcrun` on macOS and `PATH` on Linux.
+Set `LLVM_PROFDATA` to the matching tool when using a different Clang
+installation.
+
+```bash
+# Optionally set a longer training duration (default: 15s per script)
+python3 tools/pgo/pgo.py --duration=30
+```
+
+## Clang Profile Collection
+
+On all platforms, `pgo.py` collects raw profiles in a fresh directory and
+replaces `node.profdata` after a successful merge. It removes raw profiles
+after success and preserves them if training or merging fails. Training
+failures stop the script so the workloads can be fixed before trying again.
+
 ## Training Scripts

 All scripts use only Node.js built-in modules (no npm dependencies).
@@ -110,7 +144,7 @@ Each script is run as a separate process via `fork()`.
 ### Running the Orchestrator Directly

 The orchestrator can also be invoked directly (e.g. for testing individual
-workloads). When used with `pgo.ps1`, this is handled automatically.
+workloads). When used with `pgo.py`, this is handled automatically.

 ```bash
 # Run all scripts
@@ -131,7 +165,7 @@ automatically from the `--duration` flag (in seconds).

 ```
 tools/pgo/
-├── pgo.ps1                 # Windows training driver (collect + merge)
+├── pgo.py                  # Clang training driver (collect + merge)
 ├── pgo-run-all.js          # Training orchestrator
 ├── pgo-http-server.js      # HTTP server + client workload
 ├── pgo-json.js             # JSON parse/stringify workload
diff --git a/tools/pgo/pgo.ps1 b/tools/pgo/pgo.ps1
deleted file mode 100644
index 1cfedddd9e6..00000000000
--- a/tools/pgo/pgo.ps1
+++ /dev/null
@@ -1,169 +0,0 @@
-# PGO (Profile-Guided Optimization) training script for Node.js (Clang / LLVM)
-#
-# Runs PGO training workloads against an instrumented Node.js binary
-# (Release\node.exe) and merges the resulting .profraw files into
-# node.profdata for use with -fprofile-use.
-#
-# Usage (from a VS Developer Command Prompt, at the repo root):
-#   powershell -ExecutionPolicy Bypass -File .\tools\pgo\pgo.ps1
-#   powershell -ExecutionPolicy Bypass -File .\tools\pgo\pgo.ps1 -Duration 30
-#
-# The script is unsigned, so the default execution policy blocks it without
-# -ExecutionPolicy Bypass. Default duration is 15s per workload.
-#
-# Prerequisites:
-#   - Release\node.exe must be an instrumented build (built with pgo-generate)
-#   - llvm-profdata must be available (shipped with VS LLVM toolset)
-#
-# Output:
-#   - node.profdata in the repo root (ready for vcbuild.bat pgo-use)
-
-param(
-    [int]$Duration = 15
-)
-
-Set-StrictMode -Version Latest
-$ErrorActionPreference = 'Stop'
-
-# The instrumented binary and the merged profile both live at the repo root,
-# two levels up from tools\pgo. common.gypi reads node.profdata from there.
-$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path
-
-# ---------------------------------------------------------------------------
-# Locate llvm-profdata shipped with Visual Studio's LLVM toolset
-# ---------------------------------------------------------------------------
-
-function Find-LlvmProfdata {
-    # vcbuild.bat uses %VCINSTALLDIR%\Tools\Llvm\x64\bin for clang.exe - same spot for profdata
-    $vcInstallDir = $env:VCINSTALLDIR
-
-    if ($vcInstallDir) {
-        $candidate = Join-Path $vcInstallDir "Tools\Llvm\x64\bin\llvm-profdata.exe"
-        if (Test-Path $candidate) {
-            return $candidate
-        }
-    }
-
-    # Fallback: try VS 2022 / 2026 default install locations
-    $vsPaths = @(
-        "${env:ProgramFiles}\Microsoft Visual Studio\2026\Enterprise\VC\Tools\Llvm\x64\bin",
-        "${env:ProgramFiles}\Microsoft Visual Studio\2026\Community\VC\Tools\Llvm\x64\bin",
-        "${env:ProgramFiles}\Microsoft Visual Studio\2022\Enterprise\VC\Tools\Llvm\x64\bin",
-        "${env:ProgramFiles}\Microsoft Visual Studio\2022\Community\VC\Tools\Llvm\x64\bin"
-    )
-    foreach ($dir in $vsPaths) {
-        $candidate = Join-Path $dir "llvm-profdata.exe"
-        if (Test-Path $candidate) {
-            return $candidate
-        }
-    }
-
-    # Last resort: PATH
-    $fromPath = Get-Command llvm-profdata -ErrorAction SilentlyContinue
-    if ($fromPath) {
-        return $fromPath.Source
-    }
-
-    return $null
-}
-
-# ---------------------------------------------------------------------------
-# Validate prerequisites
-# ---------------------------------------------------------------------------
-
-$instrumentedNode = Join-Path $repoRoot "Release\node.exe"
-if (-not (Test-Path $instrumentedNode)) {
-    Write-Error "Instrumented binary not found: $instrumentedNode`nBuild with: vcbuild.bat pgo-generate"
-    exit 1
-}
-
-$pgoRunAll = Join-Path $PSScriptRoot "pgo-run-all.js"
-if (-not (Test-Path $pgoRunAll)) {
-    Write-Error "PGO training script not found: $pgoRunAll"
-    exit 1
-}
-
-$llvmProfdata = Find-LlvmProfdata
-if (-not $llvmProfdata) {
-    Write-Error "llvm-profdata not found. Install the LLVM toolset via Visual Studio Installer."
-    exit 1
-}
-
-# ---------------------------------------------------------------------------
-# STEP 1 – Run workloads with the instrumented binary to collect profiles
-# ---------------------------------------------------------------------------
-
-Write-Host "`n=== STEP 1: Collect PGO profiles ===" -ForegroundColor Cyan
-
-# Directory that will receive .profraw files from the instrumented binary.
-# %p (PID) and %m (module hash) keep concurrent/fork'd processes from colliding.
-$profileDir = Join-Path $repoRoot "pgo-profiles"
-
-if (Test-Path $profileDir) {
-    Remove-Item -Recurse -Force $profileDir
-}
-New-Item -ItemType Directory -Path $profileDir | Out-Null
-
-$env:LLVM_PROFILE_FILE = Join-Path $profileDir "node-%p-%m.profraw"
-
-Write-Host "Instrumented node : $instrumentedNode"
-Write-Host "Profile output    : $($env:LLVM_PROFILE_FILE)"
-Write-Host "Duration per script: ${Duration}s"
-Write-Host ""
-
-$sw = [System.Diagnostics.Stopwatch]::StartNew()
-$proc = Start-Process `
-    -FilePath $instrumentedNode `
-    -ArgumentList "`"$pgoRunAll`" --verbose --duration=$Duration" `
-    -Wait -PassThru -NoNewWindow
-$sw.Stop()
-Write-Host ("PGO training completed in {0}m {1}s (exit code: {2})" -f `
-    $sw.Elapsed.Minutes, $sw.Elapsed.Seconds, $proc.ExitCode)
-if ($proc.ExitCode -ne 0) {
-    Write-Warning "PGO training exited with code $($proc.ExitCode) - continuing with merge"
-}
-
-# Remove the env var so subsequent builds are not affected
-Remove-Item Env:\LLVM_PROFILE_FILE -ErrorAction SilentlyContinue
-
-# ---------------------------------------------------------------------------
-# STEP 2 – Merge .profraw files -> node.profdata
-# ---------------------------------------------------------------------------
-
-Write-Host "`n=== STEP 2: Merge profile data ===" -ForegroundColor Cyan
-
-Write-Host "Using llvm-profdata: $llvmProfdata"
-
-$profrawFiles = Get-ChildItem -Path $profileDir -Filter "*.profraw" -ErrorAction SilentlyContinue
-if ($profrawFiles.Count -eq 0) {
-    Write-Error "No .profraw files found in '$profileDir'. The instrumented binary may not have generated profile data."
-    exit 1
-}
-
-$totalSize = ($profrawFiles | Measure-Object -Property Length -Sum).Sum
-$totalSizeMB = [math]::Round($totalSize / 1MB, 1)
-Write-Host "Found $($profrawFiles.Count) .profraw file(s), ${totalSizeMB} MB total"
-
-$profdata = Join-Path $repoRoot "node.profdata"
-$mergeArgs = @("merge", "--output=$profdata") + ($profrawFiles | Select-Object -ExpandProperty FullName)
-
-$mergeStopwatch = [System.Diagnostics.Stopwatch]::StartNew()
-& $llvmProfdata @mergeArgs
-$mergeExitCode = $LASTEXITCODE
-$mergeStopwatch.Stop()
-
-if ($mergeExitCode -ne 0) {
-    Write-Error "llvm-profdata merge failed (exit code $mergeExitCode)"
-    exit $mergeExitCode
-}
-
-$profdataSize = [math]::Round((Get-Item $profdata).Length / 1MB, 1)
-Write-Host "Merge completed in $([math]::Round($mergeStopwatch.Elapsed.TotalSeconds, 1))s"
-
-# Clean up .profraw files now that they've been merged
-Remove-Item -Recurse -Force $profileDir
-Write-Host "Removed $($profrawFiles.Count) .profraw file(s) (${totalSizeMB} MB reclaimed)"
-
-Write-Host "`n=== PGO training complete ===" -ForegroundColor Green
-Write-Host "  Profile data: $profdata (${profdataSize} MB)"
-Write-Host "  Next step:    vcbuild.bat pgo-use"
diff --git a/tools/pgo/pgo.py b/tools/pgo/pgo.py
new file mode 100644
index 00000000000..7847b769843
--- /dev/null
+++ b/tools/pgo/pgo.py
@@ -0,0 +1,92 @@
+#!/usr/bin/env python3
+"""Train a Clang PGO build and merge its profiles into node.profdata."""
+
+import argparse
+import os
+from pathlib import Path
+import shutil
+import subprocess
+import sys
+import tempfile
+
+
+def find_llvm_profdata():
+  override = os.environ.get('LLVM_PROFDATA')
+  if override:
+    tool = shutil.which(override)
+  elif sys.platform == 'darwin':
+    tool = subprocess.check_output(
+      ['xcrun', '--find', 'llvm-profdata'], text=True).strip()
+  else:
+    candidates = []
+    if sys.platform == 'win32':
+      vc_install = os.environ.get('VCINSTALLDIR')
+      if vc_install:
+        candidates.append(Path(vc_install) / 'Tools/Llvm/x64/bin/llvm-profdata.exe')
+      program_files = os.environ.get('ProgramFiles')
+      if program_files:
+        candidates.extend(
+          Path(program_files) / 'Microsoft Visual Studio' / version / edition /
+          'VC/Tools/Llvm/x64/bin/llvm-profdata.exe'
+          for version in ('2026', '2022')
+          for edition in ('Enterprise', 'Community', 'Professional', 'BuildTools'))
+    tool = next((str(path) for path in candidates if path.is_file()), None)
+    if tool is None:
+      tool = shutil.which('llvm-profdata')
+  if not tool:
+    raise ValueError('llvm-profdata not found. Set LLVM_PROFDATA to the tool '
+                     'from your Clang toolchain.')
+  return tool
+
+
+def main():
+  parser = argparse.ArgumentParser(description=__doc__)
+  parser.add_argument('--duration', type=int, default=15,
+                      help='seconds per workload (default: 15)')
+  args = parser.parse_args()
+  if args.duration <= 0:
+    parser.error('--duration must be a positive integer')
+
+  repo_root = Path(__file__).resolve().parents[2]
+  if sys.platform == 'win32':
+    node = repo_root / 'Release/node.exe'
+    build = 'vcbuild.bat pgo-generate'
+    rebuild = 'vcbuild.bat pgo-use'
+  else:
+    node = repo_root / 'out/Release/node'
+    build = './configure --ninja --enable-pgo-generate && make'
+    rebuild = './configure --ninja --enable-pgo-use && make'
+  if not node.is_file():
+    raise ValueError(f'Instrumented binary not found: {node}\nBuild with: {build}')
+  llvm_profdata = find_llvm_profdata()
+
+  profile_dir = Path(tempfile.mkdtemp(prefix='pgo-profiles.', dir=repo_root))
+  env = os.environ.copy()
+  env['LLVM_PROFILE_FILE'] = str(profile_dir / 'node-%m-%p.profraw')
+
+  print(f'Training {node} for {args.duration}s per workload', flush=True)
+  print(f'Profile directory: {profile_dir}', flush=True)
+  try:
+    subprocess.run([str(node), str(repo_root / 'tools/pgo/pgo-run-all.js'),
+                    f'--duration={args.duration}', '--verbose'],
+                   cwd=repo_root, env=env, check=True)
+    profiles = list(profile_dir.glob('*.profraw'))
+    if not profiles:
+      raise ValueError(f'No .profraw files found in {profile_dir}. '
+                       'Build Node with PGO instrumentation using Clang.')
+
+    print(f'Merging {len(profiles)} profiles with {llvm_profdata}', flush=True)
+    merged = profile_dir / 'node.profdata'
+    subprocess.run([llvm_profdata, 'merge', '-o', str(merged),
+                    *map(str, profiles)], check=True)
+    merged.replace(repo_root / 'node.profdata')
+  except (OSError, ValueError, subprocess.CalledProcessError):
+    print(f'PGO failed. Collected profiles are in: {profile_dir}', file=sys.stderr)
+    raise
+  shutil.rmtree(profile_dir)
+  print(f'Profile data: {repo_root / "node.profdata"}')
+  print(f'Next step: {rebuild}')
+
+
+if __name__ == '__main__':
+  main()