From ba99c27ca20a39015815950a15cba259a5990dad Mon Sep 17 00:00:00 2001 From: Adriano Machado <60320+ammachado@users.noreply.github.com> Date: Fri, 18 Sep 2026 21:57:14 -0400 Subject: [PATCH 1/5] CAMEL-24829: windows-validator - temporary diagnostic for the ~24s install.ps1 launch cost Every install.ps1 launch in WebsiteInstallTest$WindowsPowerShell pays ~24s before doing any work (46 launches = 99% of the 1206s suite on PR #26568), while powershell.exe itself starts in ~0.24s. The prime suspect is the test-only CA block's Add-Type (csc.exe) compile; the second is the environment WebsiteInstallerFixture.run() clears. Add a temporary, continue-on-error step that times, twice each: bare startup and a bare Add-Type (full vs cleared env), and install.ps1 -Version 1.2.3-SNAPSHOT (rejected before any network I/O) across full/cleared env x with/without CAMEL_INSTALL_CA_CERT. The step is to be removed once the cause is confirmed and fixed. _Claude Code on behalf of Adriano Machado (@ammachado)_ _This was generated by an AI agent and may contain inaccuracies. Please verify before relying on it._ --- .../workflows/package-native-validation.yml | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/.github/workflows/package-native-validation.yml b/.github/workflows/package-native-validation.yml index faeb7df8e9192..6c7fb7b134dd6 100644 --- a/.github/workflows/package-native-validation.yml +++ b/.github/workflows/package-native-validation.yml @@ -184,6 +184,71 @@ jobs: mvn -B -P apache-snapshots -ntp \ -pl buildingtools,dsl/camel-jbang/camel-launcher install -DskipTests + # TEMPORARY (CAMEL-24829): isolates the ~24s every install.ps1 test launch pays before doing any work. + # Remove before this PR leaves draft. + - name: Diagnose install.ps1 launch cost (temporary, CAMEL-24829) + continue-on-error: true + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $installer = (Resolve-Path 'dsl/camel-jbang/camel-launcher/src/install/install.ps1').Path + $work = Join-Path $env:RUNNER_TEMP 'install-diag' + New-Item -ItemType Directory -Force -Path $work | Out-Null + + # Any PEM certificate works here: -Version 1.2.3-SNAPSHOT is rejected before the CA is used for TLS. + $root = Get-ChildItem Cert:\LocalMachine\Root | Select-Object -First 1 + $caPem = Join-Path $work 'ca.pem' + Set-Content -LiteralPath $caPem -Encoding ascii -Value ("-----BEGIN CERTIFICATE-----`n" + + [Convert]::ToBase64String($root.RawData, 'InsertLineBreaks') + "`n-----END CERTIFICATE-----") + + function Measure-Launch { + param([string] $Label, [string[]] $Arguments, [switch] $ClearedEnv, [hashtable] $ExtraEnv = @{}) + $fresh = New-Item -ItemType Directory -Path (Join-Path $work ([guid]::NewGuid().ToString('N'))) + $psi = [System.Diagnostics.ProcessStartInfo]::new('powershell.exe') + foreach ($a in $Arguments) { $psi.ArgumentList.Add($a) } + $psi.UseShellExecute = $false + $psi.RedirectStandardOutput = $true + $psi.RedirectStandardError = $true + if ($ClearedEnv) { + # Mirrors WebsiteInstallerFixture.run(): only these survive, plus a fresh per-launch home. + $keep = @{} + foreach ($n in 'PATH', 'SystemRoot', 'PATHEXT', 'TEMP', 'TMP') { + $keep[$n] = [Environment]::GetEnvironmentVariable($n) + } + $psi.Environment.Clear() + foreach ($n in $keep.Keys) { if ($keep[$n]) { $psi.Environment[$n] = $keep[$n] } } + $psi.Environment['OS'] = 'Windows_NT' + $psi.Environment['HOME'] = $fresh.FullName + $psi.Environment['USERPROFILE'] = $fresh.FullName + $psi.Environment['LOCALAPPDATA'] = Join-Path $fresh.FullName 'AppData\Local' + } + foreach ($k in $ExtraEnv.Keys) { $psi.Environment[$k] = $ExtraEnv[$k] } + $sw = [System.Diagnostics.Stopwatch]::StartNew() + $p = [System.Diagnostics.Process]::Start($psi) + $out = $p.StandardOutput.ReadToEndAsync() + $err = $p.StandardError.ReadToEndAsync() + if ($p.WaitForExit(180000)) { $exit = $p.ExitCode } else { $p.Kill($true); $exit = 'TIMEOUT' } + $sw.Stop() + '{0,-52} exit={1,-7} {2,6:N1} s' -f $Label, $exit, $sw.Elapsed.TotalSeconds + $e = $err.Result.Trim() + if ($e) { ' stderr: ' + ($e -split "`n")[0] } + } + + $noop = @('-NoProfile', '-Command', 'exit 0') + $addType = @('-NoProfile', '-Command', 'Add-Type -TypeDefinition ''public static class CamelDiag { public static int F() { return 1; } }''') + $snapshot = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $installer, '-Version', '1.2.3-SNAPSHOT') + $ca = @{ CAMEL_INSTALL_CA_CERT = $caPem } + foreach ($i in 1..2) { + Measure-Launch "[$i] startup only, full env" $noop + Measure-Launch "[$i] startup only, cleared env" $noop -ClearedEnv + Measure-Launch "[$i] Add-Type only, full env" $addType + Measure-Launch "[$i] Add-Type only, cleared env" $addType -ClearedEnv + Measure-Launch "[$i] install.ps1 SNAPSHOT, full env, no CA" $snapshot + Measure-Launch "[$i] install.ps1 SNAPSHOT, full env, CA" $snapshot -ExtraEnv $ca + Measure-Launch "[$i] install.ps1 SNAPSHOT, cleared env, no CA" $snapshot -ClearedEnv + Measure-Launch "[$i] install.ps1 SNAPSHOT, cleared env, CA" $snapshot -ClearedEnv -ExtraEnv $ca + } + - name: Run Windows installer unit tests shell: bash # The PowerShell suite has taken 10-20 minutes on windows-latest, straddling camel-launcher's 1200s From 913b8b320b7be270f868463b55fc1eb4ead18dba Mon Sep 17 00:00:00 2001 From: Adriano Machado <60320+ammachado@users.noreply.github.com> Date: Fri, 18 Sep 2026 22:41:52 -0400 Subject: [PATCH 2/5] CAMEL-24829: windows-validator - bisect the environment behind the install.ps1 launch cost Round 1 of the diagnostic ruled out the suspected CA seam: install.ps1 -Version 1.2.3-SNAPSHOT takes 0.2-0.5s in the runner's full environment with or without CAMEL_INSTALL_CA_CERT, but 17.6-17.9s in the cleared environment WebsiteInstallerFixture.run() uses, again with or without the CA. A bare powershell.exe start is 0.1s in both; the first cmdlet call (Join-Path, Add-Type) is what costs ~18-24s in the cleared environment. Replace the step with a bisection: a Join-Path probe in the cleared environment with one thing restored at a time (PSModulePath, a shared PSModuleAnalysisCachePath, real LOCALAPPDATA, real USERPROFILE/HOME, ProgramFiles/ProgramData variables, windir/SystemDrive/APPDATA and friends), plus full-environment controls with a real and a fresh home. Still temporary; removed once the cause is fixed. _Claude Code on behalf of Adriano Machado (@ammachado)_ _This was generated by an AI agent and may contain inaccuracies. Please verify before relying on it._ --- .../workflows/package-native-validation.yml | 68 +++++++++++-------- 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/.github/workflows/package-native-validation.yml b/.github/workflows/package-native-validation.yml index 6c7fb7b134dd6..4f344adc8317c 100644 --- a/.github/workflows/package-native-validation.yml +++ b/.github/workflows/package-native-validation.yml @@ -184,8 +184,9 @@ jobs: mvn -B -P apache-snapshots -ntp \ -pl buildingtools,dsl/camel-jbang/camel-launcher install -DskipTests - # TEMPORARY (CAMEL-24829): isolates the ~24s every install.ps1 test launch pays before doing any work. - # Remove before this PR leaves draft. + # TEMPORARY (CAMEL-24829): round 1 showed the ~20s per install.ps1 test launch comes from the environment + # WebsiteInstallerFixture.run() gives the child (first cmdlet call is slow), not from the CA Add-Type seam. + # This round bisects which part of that environment is responsible. Remove before this PR leaves draft. - name: Diagnose install.ps1 launch cost (temporary, CAMEL-24829) continue-on-error: true shell: pwsh @@ -194,33 +195,34 @@ jobs: $installer = (Resolve-Path 'dsl/camel-jbang/camel-launcher/src/install/install.ps1').Path $work = Join-Path $env:RUNNER_TEMP 'install-diag' New-Item -ItemType Directory -Force -Path $work | Out-Null - - # Any PEM certificate works here: -Version 1.2.3-SNAPSHOT is rejected before the CA is used for TLS. - $root = Get-ChildItem Cert:\LocalMachine\Root | Select-Object -First 1 - $caPem = Join-Path $work 'ca.pem' - Set-Content -LiteralPath $caPem -Encoding ascii -Value ("-----BEGIN CERTIFICATE-----`n" + - [Convert]::ToBase64String($root.RawData, 'InsertLineBreaks') + "`n-----END CERTIFICATE-----") + $sharedCache = Join-Path $work 'shared-cache\ModuleAnalysisCache' + New-Item -ItemType Directory -Force -Path (Split-Path $sharedCache) | Out-Null function Measure-Launch { - param([string] $Label, [string[]] $Arguments, [switch] $ClearedEnv, [hashtable] $ExtraEnv = @{}) - $fresh = New-Item -ItemType Directory -Path (Join-Path $work ([guid]::NewGuid().ToString('N'))) + param([string] $Label, [string[]] $Arguments, [switch] $FullEnv, [switch] $RealHome, + [switch] $RealLocalAppData, [string[]] $Inherit = @(), [hashtable] $ExtraEnv = @{}) + $fresh = (New-Item -ItemType Directory -Path (Join-Path $work ([guid]::NewGuid().ToString('N')))).FullName $psi = [System.Diagnostics.ProcessStartInfo]::new('powershell.exe') foreach ($a in $Arguments) { $psi.ArgumentList.Add($a) } $psi.UseShellExecute = $false $psi.RedirectStandardOutput = $true $psi.RedirectStandardError = $true - if ($ClearedEnv) { - # Mirrors WebsiteInstallerFixture.run(): only these survive, plus a fresh per-launch home. + if (-not $FullEnv) { + # Mirrors WebsiteInstallerFixture.run(): only these survive, plus $Inherit for bisection. $keep = @{} - foreach ($n in 'PATH', 'SystemRoot', 'PATHEXT', 'TEMP', 'TMP') { + foreach ($n in @('PATH', 'SystemRoot', 'PATHEXT', 'TEMP', 'TMP') + $Inherit) { $keep[$n] = [Environment]::GetEnvironmentVariable($n) } $psi.Environment.Clear() foreach ($n in $keep.Keys) { if ($keep[$n]) { $psi.Environment[$n] = $keep[$n] } } $psi.Environment['OS'] = 'Windows_NT' - $psi.Environment['HOME'] = $fresh.FullName - $psi.Environment['USERPROFILE'] = $fresh.FullName - $psi.Environment['LOCALAPPDATA'] = Join-Path $fresh.FullName 'AppData\Local' + } + if (-not $RealHome) { + $psi.Environment['HOME'] = $fresh + $psi.Environment['USERPROFILE'] = $fresh + } + if (-not $RealLocalAppData) { + $psi.Environment['LOCALAPPDATA'] = Join-Path $fresh 'AppData\Local' } foreach ($k in $ExtraEnv.Keys) { $psi.Environment[$k] = $ExtraEnv[$k] } $sw = [System.Diagnostics.Stopwatch]::StartNew() @@ -229,25 +231,35 @@ jobs: $err = $p.StandardError.ReadToEndAsync() if ($p.WaitForExit(180000)) { $exit = $p.ExitCode } else { $p.Kill($true); $exit = 'TIMEOUT' } $sw.Stop() - '{0,-52} exit={1,-7} {2,6:N1} s' -f $Label, $exit, $sw.Elapsed.TotalSeconds + '{0,-62} exit={1,-7} {2,6:N1} s' -f $Label, $exit, $sw.Elapsed.TotalSeconds $e = $err.Result.Trim() if ($e) { ' stderr: ' + ($e -split "`n")[0] } } - $noop = @('-NoProfile', '-Command', 'exit 0') - $addType = @('-NoProfile', '-Command', 'Add-Type -TypeDefinition ''public static class CamelDiag { public static int F() { return 1; } }''') + 'Runner values (for reference):' + foreach ($n in 'PSModulePath', 'LOCALAPPDATA', 'USERPROFILE', 'ProgramFiles', 'windir') { + ' {0} = {1}' -f $n, [Environment]::GetEnvironmentVariable($n) + } + $probe = @('-NoProfile', '-Command', 'Join-Path a b | Out-Null') $snapshot = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $installer, '-Version', '1.2.3-SNAPSHOT') - $ca = @{ CAMEL_INSTALL_CA_CERT = $caPem } + $programFiles = @('ProgramFiles', 'ProgramFiles(x86)', 'ProgramW6432', 'CommonProgramFiles', + 'CommonProgramFiles(x86)', 'CommonProgramW6432', 'ProgramData') + $system = @('windir', 'SystemDrive', 'ComSpec', 'PROCESSOR_ARCHITECTURE', 'NUMBER_OF_PROCESSORS', + 'COMPUTERNAME', 'USERNAME', 'USERDOMAIN', 'APPDATA') foreach ($i in 1..2) { - Measure-Launch "[$i] startup only, full env" $noop - Measure-Launch "[$i] startup only, cleared env" $noop -ClearedEnv - Measure-Launch "[$i] Add-Type only, full env" $addType - Measure-Launch "[$i] Add-Type only, cleared env" $addType -ClearedEnv - Measure-Launch "[$i] install.ps1 SNAPSHOT, full env, no CA" $snapshot - Measure-Launch "[$i] install.ps1 SNAPSHOT, full env, CA" $snapshot -ExtraEnv $ca - Measure-Launch "[$i] install.ps1 SNAPSHOT, cleared env, no CA" $snapshot -ClearedEnv - Measure-Launch "[$i] install.ps1 SNAPSHOT, cleared env, CA" $snapshot -ClearedEnv -ExtraEnv $ca + Measure-Launch "[$i] probe: full env, real home" $probe -FullEnv -RealHome -RealLocalAppData + Measure-Launch "[$i] probe: full env, fresh home" $probe -FullEnv + Measure-Launch "[$i] probe: cleared (fixture baseline)" $probe + Measure-Launch "[$i] probe: cleared + PSModulePath" $probe -Inherit 'PSModulePath' + Measure-Launch "[$i] probe: cleared + shared PSModuleAnalysisCachePath" $probe -ExtraEnv @{ PSModuleAnalysisCachePath = $sharedCache } + Measure-Launch "[$i] probe: cleared + real LOCALAPPDATA" $probe -RealLocalAppData + Measure-Launch "[$i] probe: cleared + real USERPROFILE/HOME" $probe -RealHome + Measure-Launch "[$i] probe: cleared + ProgramFiles/ProgramData vars" $probe -Inherit $programFiles + Measure-Launch "[$i] probe: cleared + windir/SystemDrive/APPDATA/etc." $probe -Inherit $system + Measure-Launch "[$i] probe: cleared + all of the above vars" $probe -Inherit (@('PSModulePath') + $programFiles + $system) + Measure-Launch "[$i] install.ps1 SNAPSHOT: cleared + all of the above vars" $snapshot -Inherit (@('PSModulePath') + $programFiles + $system) } + 'Shared cache file after both rounds: ' + (Test-Path -LiteralPath $sharedCache) - name: Run Windows installer unit tests shell: bash From de241bf43da2314fc81b6dddd930a660e34e91a5 Mon Sep 17 00:00:00 2001 From: Adriano Machado <60320+ammachado@users.noreply.github.com> Date: Fri, 18 Sep 2026 22:51:06 -0400 Subject: [PATCH 3/5] CAMEL-24829: WebsiteInstallerFixture - pass PSModulePath to install.ps1 on Windows WebsiteInstallerFixture.run() clears the child environment. Without PSModulePath, Windows PowerShell's first cmdlet call (install.ps1's Join-Path on line 29) spends ~18s in command discovery on windows-latest, so every install.ps1 run in WebsiteInstallTest$WindowsPowerShell cost ~25s and the suite took 10-20 minutes. Two diagnostic CI rounds on this PR pinned it down: a Join-Path probe in the fixture's cleared environment took 17.7s, and 0.2s with only PSModulePath restored; restoring LOCALAPPDATA, USERPROFILE/HOME, ProgramFiles/ProgramData, windir/SystemDrive/APPDATA or a shared PSModuleAnalysisCachePath each left it at ~18s. The test-only CA Add-Type block added under 0.2s. The per-test HOME/USERPROFILE/ LOCALAPPDATA isolation is unaffected (full env + fresh home: 0.2s). Drop the PSModuleAnalysisCachePath setting added by CAMEL-24485, which the same measurement showed has no effect, and remove the temporary diagnostic step. Production is unaffected: SelfUpdateCommand only clears the installer's environment under its test override. _Claude Code on behalf of Adriano Machado (@ammachado)_ _This was generated by an AI agent and may contain inaccuracies. Please verify before relying on it._ --- .../workflows/package-native-validation.yml | 77 ------------------- .../launcher/WebsiteInstallerFixture.java | 15 ++-- 2 files changed, 8 insertions(+), 84 deletions(-) diff --git a/.github/workflows/package-native-validation.yml b/.github/workflows/package-native-validation.yml index 4f344adc8317c..faeb7df8e9192 100644 --- a/.github/workflows/package-native-validation.yml +++ b/.github/workflows/package-native-validation.yml @@ -184,83 +184,6 @@ jobs: mvn -B -P apache-snapshots -ntp \ -pl buildingtools,dsl/camel-jbang/camel-launcher install -DskipTests - # TEMPORARY (CAMEL-24829): round 1 showed the ~20s per install.ps1 test launch comes from the environment - # WebsiteInstallerFixture.run() gives the child (first cmdlet call is slow), not from the CA Add-Type seam. - # This round bisects which part of that environment is responsible. Remove before this PR leaves draft. - - name: Diagnose install.ps1 launch cost (temporary, CAMEL-24829) - continue-on-error: true - shell: pwsh - run: | - $ErrorActionPreference = 'Stop' - $installer = (Resolve-Path 'dsl/camel-jbang/camel-launcher/src/install/install.ps1').Path - $work = Join-Path $env:RUNNER_TEMP 'install-diag' - New-Item -ItemType Directory -Force -Path $work | Out-Null - $sharedCache = Join-Path $work 'shared-cache\ModuleAnalysisCache' - New-Item -ItemType Directory -Force -Path (Split-Path $sharedCache) | Out-Null - - function Measure-Launch { - param([string] $Label, [string[]] $Arguments, [switch] $FullEnv, [switch] $RealHome, - [switch] $RealLocalAppData, [string[]] $Inherit = @(), [hashtable] $ExtraEnv = @{}) - $fresh = (New-Item -ItemType Directory -Path (Join-Path $work ([guid]::NewGuid().ToString('N')))).FullName - $psi = [System.Diagnostics.ProcessStartInfo]::new('powershell.exe') - foreach ($a in $Arguments) { $psi.ArgumentList.Add($a) } - $psi.UseShellExecute = $false - $psi.RedirectStandardOutput = $true - $psi.RedirectStandardError = $true - if (-not $FullEnv) { - # Mirrors WebsiteInstallerFixture.run(): only these survive, plus $Inherit for bisection. - $keep = @{} - foreach ($n in @('PATH', 'SystemRoot', 'PATHEXT', 'TEMP', 'TMP') + $Inherit) { - $keep[$n] = [Environment]::GetEnvironmentVariable($n) - } - $psi.Environment.Clear() - foreach ($n in $keep.Keys) { if ($keep[$n]) { $psi.Environment[$n] = $keep[$n] } } - $psi.Environment['OS'] = 'Windows_NT' - } - if (-not $RealHome) { - $psi.Environment['HOME'] = $fresh - $psi.Environment['USERPROFILE'] = $fresh - } - if (-not $RealLocalAppData) { - $psi.Environment['LOCALAPPDATA'] = Join-Path $fresh 'AppData\Local' - } - foreach ($k in $ExtraEnv.Keys) { $psi.Environment[$k] = $ExtraEnv[$k] } - $sw = [System.Diagnostics.Stopwatch]::StartNew() - $p = [System.Diagnostics.Process]::Start($psi) - $out = $p.StandardOutput.ReadToEndAsync() - $err = $p.StandardError.ReadToEndAsync() - if ($p.WaitForExit(180000)) { $exit = $p.ExitCode } else { $p.Kill($true); $exit = 'TIMEOUT' } - $sw.Stop() - '{0,-62} exit={1,-7} {2,6:N1} s' -f $Label, $exit, $sw.Elapsed.TotalSeconds - $e = $err.Result.Trim() - if ($e) { ' stderr: ' + ($e -split "`n")[0] } - } - - 'Runner values (for reference):' - foreach ($n in 'PSModulePath', 'LOCALAPPDATA', 'USERPROFILE', 'ProgramFiles', 'windir') { - ' {0} = {1}' -f $n, [Environment]::GetEnvironmentVariable($n) - } - $probe = @('-NoProfile', '-Command', 'Join-Path a b | Out-Null') - $snapshot = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $installer, '-Version', '1.2.3-SNAPSHOT') - $programFiles = @('ProgramFiles', 'ProgramFiles(x86)', 'ProgramW6432', 'CommonProgramFiles', - 'CommonProgramFiles(x86)', 'CommonProgramW6432', 'ProgramData') - $system = @('windir', 'SystemDrive', 'ComSpec', 'PROCESSOR_ARCHITECTURE', 'NUMBER_OF_PROCESSORS', - 'COMPUTERNAME', 'USERNAME', 'USERDOMAIN', 'APPDATA') - foreach ($i in 1..2) { - Measure-Launch "[$i] probe: full env, real home" $probe -FullEnv -RealHome -RealLocalAppData - Measure-Launch "[$i] probe: full env, fresh home" $probe -FullEnv - Measure-Launch "[$i] probe: cleared (fixture baseline)" $probe - Measure-Launch "[$i] probe: cleared + PSModulePath" $probe -Inherit 'PSModulePath' - Measure-Launch "[$i] probe: cleared + shared PSModuleAnalysisCachePath" $probe -ExtraEnv @{ PSModuleAnalysisCachePath = $sharedCache } - Measure-Launch "[$i] probe: cleared + real LOCALAPPDATA" $probe -RealLocalAppData - Measure-Launch "[$i] probe: cleared + real USERPROFILE/HOME" $probe -RealHome - Measure-Launch "[$i] probe: cleared + ProgramFiles/ProgramData vars" $probe -Inherit $programFiles - Measure-Launch "[$i] probe: cleared + windir/SystemDrive/APPDATA/etc." $probe -Inherit $system - Measure-Launch "[$i] probe: cleared + all of the above vars" $probe -Inherit (@('PSModulePath') + $programFiles + $system) - Measure-Launch "[$i] install.ps1 SNAPSHOT: cleared + all of the above vars" $snapshot -Inherit (@('PSModulePath') + $programFiles + $system) - } - 'Shared cache file after both rounds: ' + (Test-Path -LiteralPath $sharedCache) - - name: Run Windows installer unit tests shell: bash # The PowerShell suite has taken 10-20 minutes on windows-latest, straddling camel-launcher's 1200s diff --git a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteInstallerFixture.java b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteInstallerFixture.java index 996e2eddf74f1..728fdb6431cdc 100644 --- a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteInstallerFixture.java +++ b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteInstallerFixture.java @@ -64,8 +64,6 @@ final class WebsiteInstallerFixture implements AutoCloseable { private static final String KEY_ALIAS = "camel-installer-test"; private static final String DEFAULT_BASE_VERSION = "9.9.9"; private static final Duration PROCESS_TIMEOUT = Duration.ofSeconds(60); - private static final Path POWERSHELL_MODULE_ANALYSIS_CACHE - = Path.of("target", "powershell", "ModuleAnalysisCache").toAbsolutePath(); record Result(int exit, String stdout, String stderr) { } @@ -457,11 +455,14 @@ Result run(List command, Map env) throws Exception { pb.environment().put(name, value); } } - // PowerShell keeps its module analysis cache under LOCALAPPDATA, which every test points at a - // fresh home, so each powershell.exe launch would rebuild it from scratch when auto-loading - // Expand-Archive / Get-FileHash. Share one cache file across the whole test run instead. - Files.createDirectories(POWERSHELL_MODULE_ANALYSIS_CACHE.getParent()); - pb.environment().put("PSModuleAnalysisCachePath", POWERSHELL_MODULE_ANALYSIS_CACHE.toString()); + // Without PSModulePath, Windows PowerShell's first cmdlet call (install.ps1's Join-Path on line 29) + // spends ~18s in command discovery on windows-latest, which made every install.ps1 run in this + // suite cost ~25s. Measured on CI: a Join-Path probe in this cleared environment took 17.7s, and + // 0.2s with only PSModulePath restored. The per-test HOME/USERPROFILE/LOCALAPPDATA are unaffected. + String psModulePath = System.getenv("PSModulePath"); + if (psModulePath != null) { + pb.environment().put("PSModulePath", psModulePath); + } } pb.environment().putAll(env); String home = env.get("HOME"); From d5c338d57a54534b35ccb856e17af94ccf0537bb Mon Sep 17 00:00:00 2001 From: Adriano Machado <60320+ammachado@users.noreply.github.com> Date: Fri, 18 Sep 2026 23:16:15 -0400 Subject: [PATCH 4/5] CAMEL-24829: WebsiteInstallerFixture - pin PSModulePath to Windows PowerShell's built-in modules Passing the runner's PSModulePath through did not help: install.ps1 runs in the suite still took ~19.7s (median of 46). The round 2 probe that restored PSModulePath ran under pwsh, which prepends its own module directories to PSModulePath at startup; the tests run from bash, whose PSModulePath does not have them. Set the child's PSModulePath to %SystemRoot%\System32\WindowsPowerShell\v1.0\Modules instead, which holds every cmdlet install.ps1 uses and keeps the suite independent of whatever the runner has installed. A temporary round 3 diagnostic step compares the values on CI. _Claude Code on behalf of Adriano Machado (@ammachado)_ _This was generated by an AI agent and may contain inaccuracies. Please verify before relying on it._ --- .../workflows/package-native-validation.yml | 65 +++++++++++++++++++ .../launcher/WebsiteInstallerFixture.java | 13 ++-- 2 files changed, 71 insertions(+), 7 deletions(-) diff --git a/.github/workflows/package-native-validation.yml b/.github/workflows/package-native-validation.yml index faeb7df8e9192..daeaa5b411786 100644 --- a/.github/workflows/package-native-validation.yml +++ b/.github/workflows/package-native-validation.yml @@ -184,6 +184,71 @@ jobs: mvn -B -P apache-snapshots -ntp \ -pl buildingtools,dsl/camel-jbang/camel-launcher install -DskipTests + # TEMPORARY (CAMEL-24829): round 2 restored PSModulePath as pwsh sees it (pwsh prepends its own module + # directories at startup), but the tests run from bash, whose PSModulePath lacks those entries. Passing + # that through did not speed the suite up. This round compares the values. Remove before leaving draft. + - name: Capture PSModulePath as bash sees it (temporary, CAMEL-24829) + shell: bash + run: echo "PSMP_FROM_BASH=$PSModulePath" >> "$GITHUB_ENV" + + - name: Diagnose install.ps1 launch cost, round 3 (temporary, CAMEL-24829) + continue-on-error: true + shell: pwsh + run: | + $installer = (Resolve-Path 'dsl/camel-jbang/camel-launcher/src/install/install.ps1').Path + $work = Join-Path $env:RUNNER_TEMP 'install-diag' + New-Item -ItemType Directory -Force -Path $work | Out-Null + + function Measure-Launch { + param([string] $Label, [string[]] $Arguments, [string] $ModulePath) + $fresh = (New-Item -ItemType Directory -Path (Join-Path $work ([guid]::NewGuid().ToString('N')))).FullName + $psi = [System.Diagnostics.ProcessStartInfo]::new('powershell.exe') + foreach ($a in $Arguments) { $psi.ArgumentList.Add($a) } + $psi.UseShellExecute = $false + $psi.RedirectStandardOutput = $true + $psi.RedirectStandardError = $true + # Mirrors WebsiteInstallerFixture.run(). + $keep = @{} + foreach ($n in 'PATH', 'SystemRoot', 'PATHEXT', 'TEMP', 'TMP') { + $keep[$n] = [Environment]::GetEnvironmentVariable($n) + } + $psi.Environment.Clear() + foreach ($n in $keep.Keys) { if ($keep[$n]) { $psi.Environment[$n] = $keep[$n] } } + $psi.Environment['OS'] = 'Windows_NT' + $psi.Environment['HOME'] = $fresh + $psi.Environment['USERPROFILE'] = $fresh + $psi.Environment['LOCALAPPDATA'] = Join-Path $fresh 'AppData\Local' + if ($ModulePath) { $psi.Environment['PSModulePath'] = $ModulePath } + $sw = [System.Diagnostics.Stopwatch]::StartNew() + $p = [System.Diagnostics.Process]::Start($psi) + $out = $p.StandardOutput.ReadToEndAsync() + $err = $p.StandardError.ReadToEndAsync() + if ($p.WaitForExit(180000)) { $exit = $p.ExitCode } else { $p.Kill($true); $exit = 'TIMEOUT' } + $sw.Stop() + '{0,-62} exit={1,-7} {2,6:N1} s' -f $Label, $exit, $sw.Elapsed.TotalSeconds + $e = $err.Result.Trim() + if ($e) { ' stderr: ' + ($e -split "`n")[0] } + } + + $system = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\Modules' + $programFiles = Join-Path $env:ProgramFiles 'WindowsPowerShell\Modules' + 'PSModulePath (pwsh): ' + $env:PSModulePath + 'PSModulePath (bash): ' + $env:PSMP_FROM_BASH + 'Modules under ' + $programFiles + ': ' + ((Get-ChildItem -Directory $programFiles).Name -join ', ') + $probe = @('-NoProfile', '-Command', 'Join-Path a b | Out-Null') + $snapshot = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $installer, '-Version', '1.2.3-SNAPSHOT') + foreach ($i in 1..2) { + Measure-Launch "[$i] probe: no PSModulePath (old fixture)" $probe + Measure-Launch "[$i] probe: pwsh's PSModulePath" $probe $env:PSModulePath + Measure-Launch "[$i] probe: bash's PSModulePath (first fix)" $probe $env:PSMP_FROM_BASH + Measure-Launch "[$i] probe: System32 modules only (this fix)" $probe $system + Measure-Launch "[$i] probe: ProgramFiles + System32" $probe "$programFiles;$system" + foreach ($entry in ($env:PSMP_FROM_BASH -split ';' | Where-Object { $_ })) { + Measure-Launch "[$i] probe: bash entry only: $entry" $probe $entry + } + Measure-Launch "[$i] install.ps1 SNAPSHOT: System32 modules only" $snapshot $system + } + - name: Run Windows installer unit tests shell: bash # The PowerShell suite has taken 10-20 minutes on windows-latest, straddling camel-launcher's 1200s diff --git a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteInstallerFixture.java b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteInstallerFixture.java index 728fdb6431cdc..4dc7d69255bc5 100644 --- a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteInstallerFixture.java +++ b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteInstallerFixture.java @@ -455,13 +455,12 @@ Result run(List command, Map env) throws Exception { pb.environment().put(name, value); } } - // Without PSModulePath, Windows PowerShell's first cmdlet call (install.ps1's Join-Path on line 29) - // spends ~18s in command discovery on windows-latest, which made every install.ps1 run in this - // suite cost ~25s. Measured on CI: a Join-Path probe in this cleared environment took 17.7s, and - // 0.2s with only PSModulePath restored. The per-test HOME/USERPROFILE/LOCALAPPDATA are unaffected. - String psModulePath = System.getenv("PSModulePath"); - if (psModulePath != null) { - pb.environment().put("PSModulePath", psModulePath); + // Limit module discovery to Windows PowerShell's built-in modules, which hold every cmdlet + // install.ps1 uses. Without PSModulePath, or with the runner's own, the first cmdlet call + // (install.ps1's Join-Path on line 29) spends ~18s searching the other module directories on + // windows-latest, which made every install.ps1 run in this suite cost ~20s. + if (sysRoot != null) { + pb.environment().put("PSModulePath", sysRoot + "\\System32\\WindowsPowerShell\\v1.0\\Modules"); } } pb.environment().putAll(env); From 615cea22ad96098514493009dce660b04821f3b4 Mon Sep 17 00:00:00 2001 From: Adriano Machado <60320+ammachado@users.noreply.github.com> Date: Fri, 18 Sep 2026 23:42:27 -0400 Subject: [PATCH 5/5] CAMEL-24829: WebsiteInstallerFixture - point PSModulePath at the test home's user module directory Round 3 of the diagnostic (run 35418075426), a Join-Path probe in the fixture's cleared environment, launched from bash like the tests: - PSModulePath unset, bash's value, System32 modules only, Program Files modules only, Program Files + System32: 17.4-28.9s - pwsh's value (pwsh's own module directories come first): 0.2s - a single directory without Windows PowerShell's own modules (the Az directory, a nonexistent user directory, the SQL Server directory): 0.2-0.8s, and Join-Path still resolves So the previous commit's System32-only value did not help either (install.ps1 median 19.5s, suite 901s). Program Files\WindowsPowerShell\Modules on windows-latest holds AWSPowerShell, Microsoft.Graph and SqlServer. Point PSModulePath at \Documents\WindowsPowerShell\Modules, the isolated home's own user module directory, which does not exist. Remove the temporary diagnostic steps. _Claude Code on behalf of Adriano Machado (@ammachado)_ _This was generated by an AI agent and may contain inaccuracies. Please verify before relying on it._ --- .../workflows/package-native-validation.yml | 65 ------------------- .../launcher/WebsiteInstallerFixture.java | 17 +++-- 2 files changed, 10 insertions(+), 72 deletions(-) diff --git a/.github/workflows/package-native-validation.yml b/.github/workflows/package-native-validation.yml index daeaa5b411786..faeb7df8e9192 100644 --- a/.github/workflows/package-native-validation.yml +++ b/.github/workflows/package-native-validation.yml @@ -184,71 +184,6 @@ jobs: mvn -B -P apache-snapshots -ntp \ -pl buildingtools,dsl/camel-jbang/camel-launcher install -DskipTests - # TEMPORARY (CAMEL-24829): round 2 restored PSModulePath as pwsh sees it (pwsh prepends its own module - # directories at startup), but the tests run from bash, whose PSModulePath lacks those entries. Passing - # that through did not speed the suite up. This round compares the values. Remove before leaving draft. - - name: Capture PSModulePath as bash sees it (temporary, CAMEL-24829) - shell: bash - run: echo "PSMP_FROM_BASH=$PSModulePath" >> "$GITHUB_ENV" - - - name: Diagnose install.ps1 launch cost, round 3 (temporary, CAMEL-24829) - continue-on-error: true - shell: pwsh - run: | - $installer = (Resolve-Path 'dsl/camel-jbang/camel-launcher/src/install/install.ps1').Path - $work = Join-Path $env:RUNNER_TEMP 'install-diag' - New-Item -ItemType Directory -Force -Path $work | Out-Null - - function Measure-Launch { - param([string] $Label, [string[]] $Arguments, [string] $ModulePath) - $fresh = (New-Item -ItemType Directory -Path (Join-Path $work ([guid]::NewGuid().ToString('N')))).FullName - $psi = [System.Diagnostics.ProcessStartInfo]::new('powershell.exe') - foreach ($a in $Arguments) { $psi.ArgumentList.Add($a) } - $psi.UseShellExecute = $false - $psi.RedirectStandardOutput = $true - $psi.RedirectStandardError = $true - # Mirrors WebsiteInstallerFixture.run(). - $keep = @{} - foreach ($n in 'PATH', 'SystemRoot', 'PATHEXT', 'TEMP', 'TMP') { - $keep[$n] = [Environment]::GetEnvironmentVariable($n) - } - $psi.Environment.Clear() - foreach ($n in $keep.Keys) { if ($keep[$n]) { $psi.Environment[$n] = $keep[$n] } } - $psi.Environment['OS'] = 'Windows_NT' - $psi.Environment['HOME'] = $fresh - $psi.Environment['USERPROFILE'] = $fresh - $psi.Environment['LOCALAPPDATA'] = Join-Path $fresh 'AppData\Local' - if ($ModulePath) { $psi.Environment['PSModulePath'] = $ModulePath } - $sw = [System.Diagnostics.Stopwatch]::StartNew() - $p = [System.Diagnostics.Process]::Start($psi) - $out = $p.StandardOutput.ReadToEndAsync() - $err = $p.StandardError.ReadToEndAsync() - if ($p.WaitForExit(180000)) { $exit = $p.ExitCode } else { $p.Kill($true); $exit = 'TIMEOUT' } - $sw.Stop() - '{0,-62} exit={1,-7} {2,6:N1} s' -f $Label, $exit, $sw.Elapsed.TotalSeconds - $e = $err.Result.Trim() - if ($e) { ' stderr: ' + ($e -split "`n")[0] } - } - - $system = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\Modules' - $programFiles = Join-Path $env:ProgramFiles 'WindowsPowerShell\Modules' - 'PSModulePath (pwsh): ' + $env:PSModulePath - 'PSModulePath (bash): ' + $env:PSMP_FROM_BASH - 'Modules under ' + $programFiles + ': ' + ((Get-ChildItem -Directory $programFiles).Name -join ', ') - $probe = @('-NoProfile', '-Command', 'Join-Path a b | Out-Null') - $snapshot = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $installer, '-Version', '1.2.3-SNAPSHOT') - foreach ($i in 1..2) { - Measure-Launch "[$i] probe: no PSModulePath (old fixture)" $probe - Measure-Launch "[$i] probe: pwsh's PSModulePath" $probe $env:PSModulePath - Measure-Launch "[$i] probe: bash's PSModulePath (first fix)" $probe $env:PSMP_FROM_BASH - Measure-Launch "[$i] probe: System32 modules only (this fix)" $probe $system - Measure-Launch "[$i] probe: ProgramFiles + System32" $probe "$programFiles;$system" - foreach ($entry in ($env:PSMP_FROM_BASH -split ';' | Where-Object { $_ })) { - Measure-Launch "[$i] probe: bash entry only: $entry" $probe $entry - } - Measure-Launch "[$i] install.ps1 SNAPSHOT: System32 modules only" $snapshot $system - } - - name: Run Windows installer unit tests shell: bash # The PowerShell suite has taken 10-20 minutes on windows-latest, straddling camel-launcher's 1200s diff --git a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteInstallerFixture.java b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteInstallerFixture.java index 4dc7d69255bc5..74a4729567f12 100644 --- a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteInstallerFixture.java +++ b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteInstallerFixture.java @@ -455,15 +455,18 @@ Result run(List command, Map env) throws Exception { pb.environment().put(name, value); } } - // Limit module discovery to Windows PowerShell's built-in modules, which hold every cmdlet - // install.ps1 uses. Without PSModulePath, or with the runner's own, the first cmdlet call - // (install.ps1's Join-Path on line 29) spends ~18s searching the other module directories on - // windows-latest, which made every install.ps1 run in this suite cost ~20s. - if (sysRoot != null) { - pb.environment().put("PSModulePath", sysRoot + "\\System32\\WindowsPowerShell\\v1.0\\Modules"); - } } pb.environment().putAll(env); + String userProfile = env.get("USERPROFILE"); + if (FakeJava.WINDOWS && userProfile != null) { + // Point PSModulePath at the isolated home's own (empty) user module directory. When PSModulePath + // is unset, or lists Windows PowerShell's built-in module directory, the first cmdlet call + // (install.ps1's Join-Path on line 29) spends ~18s on windows-latest, whose + // Program Files\WindowsPowerShell\Modules holds AWSPowerShell, Microsoft.Graph and SqlServer; that + // made every install.ps1 run in this suite cost ~20s. With only an unrelated directory listed, the + // same call takes 0.2s and Windows PowerShell still finds its built-in cmdlets. + pb.environment().put("PSModulePath", Path.of(userProfile, "Documents", "WindowsPowerShell", "Modules").toString()); + } String home = env.get("HOME"); if (home != null && Files.isDirectory(Path.of(home))) { // Any accidental relative-path side effect lands in the isolated test HOME rather than