Skip to content

Rework description in CHANGES.txt for GH #4717. #4720

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from

Conversation

jcbrill
Copy link
Contributor

@jcbrill jcbrill commented May 13, 2025

Partially restore restore original GH #4717 CHANGES.txt text as a placeholder for reworked description.

Contributor Checklist:

  • I have created a new test or updated the unit tests to cover the new/changed functionality.
  • I have updated CHANGES.txt and RELEASE.txt (and read the README.rst).
  • I have updated the appropriate documentation

Changes:
* Partially restore restore original GH SCons#4717 CHANGES.txt description
* Change MSVS to MSVC in RELEASE.txt
* Remove extraneous two blank lines introduced in common.py when variable moved
@jcbrill
Copy link
Contributor Author

jcbrill commented May 13, 2025

Notes for updated text PR.

Observations:

  • Visual Studio VCPKG component was not installed as a component in windows-2019.
  • windows-2022 and windows-2025 are slow with VS VCPKG component and powershell 5
  • windows-2022 and windows-2025 are faster with VS VCPKG component and powershell 7

The runtimes with powershell 5 are an order of magnitude slower than with powershell 7 in windows-2022 and windows-2025.

Visual Studio 2022 VCPKG

Runner Visual Studio 2022 VCPKG Component
windows-2019 Not Installed
windows-2022 Microsoft.VisualStudio.Component.Vcpkg 17.13.35710.127
windows-2025 Microsoft.VisualStudio.Component.Vcpkg 17.13.35710.127

Example SConstruct times with Powershell 5 (runner results sorted low to high in secs):

Runner 1 2 3 4
windows-2019 2.9 4.1 4.7 7.4
windows-2022 39.9 61.3 69.7 99.3
windows-2025 33.0 46.5 52.4 92.6

Example SConstruct times with Powershell 7 (runner results sorted low to high in secs):

Runner 1 2 3 4
windows-2019 2.8 4.8 6.8 8.3
windows-2022 4.3 4.4 7.1 13.4
windows-2025 2.6 2.8 3.0 5.7

@jcbrill
Copy link
Contributor Author

jcbrill commented May 13, 2025

Windows runner experiments measuring only elapsed time for subprocess invocation of msvc batch files.

Notes:

  • Visual Studio VCPKG component is not installed in windows-2019 runner.
  • The elapsed times in seconds for a given windows runner can be wildly variable.
  • Elapsed times presented are not sorted.
  • windows-latest == windows-2022

Windows Runner Experiments

Powershell 5 and 7 results in seconds:

  • 4 workflow runs: E1, E2, E3, E4
  • 3 vcvars invocations: 1, 2, 3
PS Runner vcvars E1 E2 E3 E4
PS5 2019 1 1.4 3.1 5.8 7.1
PS5 2019 2 0.6 0.6 0.6 0.6
PS5 2919 3 0.6 0.6 0.6 0.6
${\color{#FF695C} \text{PS5}}$ ${\color{#FF695C} \text{2022}}$ 1 ${\color{#FF695C} \text{45.6}}$ ${\color{#FF695C} \text{64.8}}$ ${\color{#FF695C} \text{72.6}}$ ${\color{#FF695C} \text{69.4}}$
${\color{#FF695C} \text{PS5}}$ ${\color{#FF695C} \text{2022}}$ 2 ${\color{#FF695C} \text{20.8}}$ ${\color{#FF695C} \text{21.3}}$ ${\color{#FF695C} \text{21.3}}$ ${\color{#FF695C} \text{21.2}}$
${\color{#FF695C} \text{PS5}}$ ${\color{#FF695C} \text{2022}}$ 3 ${\color{#FF695C} \text{20.8}}$ ${\color{#FF695C} \text{21.2}}$ ${\color{#FF695C} \text{21.6}}$ ${\color{#FF695C} \text{21.1}}$
${\color{#FF695C} \text{PS5}}$ ${\color{#FF695C} \text{2025}}$ 1 ${\color{#FF695C} \text{32.5}}$ ${\color{#FF695C} \text{31.4}}$ ${\color{#FF695C} \text{38.4}}$ ${\color{#FF695C} \text{35.5}}$
${\color{#FF695C} \text{PS5}}$ ${\color{#FF695C} \text{2025}}$ 2 ${\color{#FF695C} \text{21.6}}$ ${\color{#FF695C} \text{21.6}}$ ${\color{#FF695C} \text{22.3}}$ ${\color{#FF695C} \text{21.3}}$
${\color{#FF695C} \text{PS5}}$ ${\color{#FF695C} \text{2025}}$ 3 ${\color{#FF695C} \text{21.6}}$ ${\color{#FF695C} \text{21.5}}$ ${\color{#FF695C} \text{22.3}}$ ${\color{#FF695C} \text{21.3}}$
PS7 2019 1 1.5 1.2 1.4 4.0
PS7 2019 2 0.6 0.6 0.6 0.6
PS7 2019 3 0.6 0.6 0.7 0.6
PS7 2022 1 6.7 2.5 7.1 13.8
PS7 2022 2 1.2 1.2 1.2 1.2
PS7 2022 3 1.2 1.2 1.2 1.2
PS7 2025 1 2.0 2.0 2.2 2.0
PS7 2025 2 1.6 1.6 1.7 1.7
PS7 2025 3 1.6 1.6 1.6 1.6

@jcbrill
Copy link
Contributor Author

jcbrill commented May 13, 2025

Windows VMWare VM Experiments

Powershell 5 and 7 results in seconds:

  • Windows 11 Pro
  • VS 2022 Enterprise w/VCPKG component
  • 4 workflow runs: E1, E2, E3, E4
  • 3 vcvars invocations: 1, 2, 3

VM State Reverted Between Experiments

PS vcvars E1 E2 E3 E4
PS5 1 2.5 2.2 2.2 2.2
PS5 2 1.4 1.2 1.3 1.3
PS5 3 1.3 1.3 1.2 1.2
PS7 1 6.7 1.9 1.9 1.9
PS7 2 1.5 1.5 1.3 1.4
PS7 3 1.5 1.3 1.6 1.3

VM State Not Reverted Between Experiments

PS vcvars E1 E2 E3 E4
PS5 1 2.3 1.2 1.2 1.2
PS5 2 1.3 1.2 1.2 1.2
PS5 3 1.3 1.2 1.3 1.2
PS7 1 2.0 1.4 1.3 1.4
PS7 2 1.3 1.3 1.4 1.3
PS7 3 1.3 1.3 1.4 1.4

@jcbrill jcbrill changed the title [WIP] Rework description in CHANGES.txt for GH #4717. Rework description in CHANGES.txt for GH #4717. May 13, 2025
@jcbrill
Copy link
Contributor Author

jcbrill commented May 13, 2025

Does the CHANGES.txt blurb make sense to anyone else?

@mwichmann
Copy link
Collaborator

Makes sense, yes.

I might be proposing a bit of a rewording which would affect tone and tense, but not content - if I get to it, I promise to be careful not to lose any of the message. I'm currently proofreading some Python docu PRs on request and the energy may not extend to this one. We'll see what @bdbaddog thinks.

@jcbrill
Copy link
Contributor Author

jcbrill commented May 14, 2025

@mwichmann I starting to wonder if the implementation should be adaptive to the os environment rather than hard-coded.

PS 7 is now hard-coded before PS 5 in the msvc environment.

The telemetry calls are currently hard-coded to use PS 5 in the batch files which is why PS 5 was added to the msvc environment.

The optional vcpkg batch files will use PS 7 or 5 based on which is found first on the os system path. The SCons behavior is only the same as the msvc batch files if PS 7 precedes PS 5 on the os system path. It does in the windows runner but it may not in every end user's system configuration.

I'm wondering if the order of PS 7 and 5 in the msvc environment should be based in the order on the user's system path. Similarly, should PSModulePath be added if it exists in the os environment and only "known" path component locations (e.g., User, All Users, and Default installation) preserved?

There are 3 known PS 7 path locations and 2 known PS 5 path locations in the PSModulePath in windows-2022. PS 7: Current user, All user, Install. PS 5: All user, Install. In windows-2022 there is a PS 5 user path in PSModulePath but it is not the current user.

This is a situation where invoking the msvc batch files in the minimal msvc environment could be different than when running in the os environment.

Any thoughts?

[Always] <VSROOT>\Common7\Tools\VsDevCmd.bat (hard-coded: powershell.exe):

@REM Send Telemetry if user's VS is opted-in
if "%VSCMD_SKIP_SENDTELEMETRY%"=="" (
    if "%VSCMD_DEBUG%" NEQ "" (
        @echo [DEBUG:%~nx0] Sending telemetry
        powershell.exe -NoProfile -Command "& {Import-Module '%~dp0\Microsoft.VisualStudio.DevShell.dll'; Send-VsDevShellTelemetry -NewInstanceType Cmd;}"
    ) else (
        START "" /B powershell.exe -NoProfile -Command "& {if($PSVersionTable.PSVersion.Major -ge 3){Import-Module '%~dp0\Microsoft.VisualStudio.DevShell.dll'; Send-VsDevShellTelemetry -NewInstanceType Cmd; }}" > NUL
    )
)

[If Installed] <VSROOT>\VC\vcpkg\vcpkg-init.cmd (first found: pwsh.exe or powershell.exe):

:: Call powershell which may or may not invoke bootstrap if there's a version mismatch
SET Z_POWERSHELL_EXE=
FOR %%i IN (pwsh.exe powershell.exe) DO (
  IF EXIST "%%~$PATH:i" (
    SET "Z_POWERSHELL_EXE=%%~$PATH:i"
    GOTO :gotpwsh
  )
)

IMPORTANT: Use of diff format in code fragments intended as easy method of highlighting rather than as actual additions and subtractions.

windows-2022:

os.environ[PATH]=
+   C:\Program Files\PowerShell\7;
    C:\Program Files\MongoDB\Server\5.0\bin;
    C:\aliyun-cli;
    C:\vcpkg;
    C:\Program Files (x86)\NSIS\;
    C:\tools\zstd;
    C:\Program Files\Mercurial\;
    C:\hostedtoolcache\windows\stack\3.5.1\x64;
    C:\cabal\bin;
    C:\\ghcup\bin;
    C:\mingw64\bin;
    C:\Program Files\dotnet;
    C:\Program Files\MySQL\MySQL Server 8.0\bin;
    C:\Program Files\R\R-4.4.2\bin\x64;
    C:\SeleniumWebDrivers\GeckoDriver;
    C:\SeleniumWebDrivers\EdgeDriver\;
    C:\SeleniumWebDrivers\ChromeDriver;
    C:\Program Files (x86)\sbt\bin;
    C:\Program Files (x86)\GitHub CLI;
    C:\Program Files\Git\bin;
    C:\Program Files (x86)\pipx_bin;
    C:\npm\prefix;
    C:\hostedtoolcache\windows\go\1.24.2\x64\bin;
    C:\hostedtoolcache\windows\Python\3.9.13\x64\Scripts;
    C:\hostedtoolcache\windows\Python\3.9.13\x64;
    C:\hostedtoolcache\windows\Ruby\3.0.7\x64\bin;
    C:\Program Files\OpenSSL\bin;
    C:\tools\kotlinc\bin;
    C:\hostedtoolcache\windows\Java_Temurin-Hotspot_jdk\8.0.452-9\x64\bin;
    C:\Program Files\ImageMagick-7.1.1-Q16-HDRI;
    C:\Program Files\Microsoft SDKs\Azure\CLI2\wbin;
    C:\ProgramData\kind;
    C:\ProgramData\Chocolatey\bin;
    C:\Windows\system32;
    C:\Windows;
    C:\Windows\System32\Wbem;
+   C:\Windows\System32\WindowsPowerShell\v1.0\;
    C:\Windows\System32\OpenSSH\;
    C:\Program Files\dotnet\;
+   C:\Program Files\PowerShell\7\;
    C:\Program Files\Microsoft\Web Platform Installer\;
    C:\Program Files\TortoiseSVN\bin;
    C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\;
    C:\Program Files\Microsoft SQL Server\150\Tools\Binn\;
    C:\Program Files (x86)\Windows Kits\10\Windows Performance Toolkit\;
    C:\Program Files (x86)\WiX Toolset v3.14\bin;
    C:\Program Files\Microsoft SQL Server\130\DTS\Binn\;
    C:\Program Files\Microsoft SQL Server\140\DTS\Binn\;
    C:\Program Files\Microsoft SQL Server\150\DTS\Binn\;
    C:\Program Files\Microsoft SQL Server\160\DTS\Binn\;
    C:\Strawberry\c\bin;
    C:\Strawberry\perl\site\bin;
    C:\Strawberry\perl\bin;
    C:\ProgramData\chocolatey\lib\pulumi\tools\Pulumi\bin;
    C:\Program Files\CMake\bin;
    C:\ProgramData\chocolatey\lib\maven\apache-maven-3.9.9\bin;
    C:\Program Files\Microsoft Service Fabric\bin\Fabric\Fabric.Code;
    C:\Program Files\Microsoft SDKs\Service Fabric\Tools\ServiceFabricLocalClusterManager;
    C:\Program Files\nodejs\;
    C:\Program Files\Git\cmd;
    C:\Program Files\Git\mingw64\bin;
    C:\Program Files\Git\usr\bin;
    C:\Program Files\GitHub CLI\;
    c:\tools\php;
    C:\Program Files (x86)\sbt\bin;
    C:\Program Files\Amazon\AWSCLIV2\;
    C:\Program Files\Amazon\SessionManagerPlugin\bin\;
    C:\Program Files\Amazon\AWSSAMCLI\bin\;
    C:\Program Files\Microsoft SQL Server\130\Tools\Binn\;
    C:\Program Files\LLVM\bin;
    C:\Users\runneradmin\.dotnet\tools;
    C:\Users\runneradmin\.cargo\bin;
    C:\Users\runneradmin\AppData\Local\Microsoft\WindowsApps

os.environ[PSMODULEPATH]=
+   C:\Users\runneradmin\Documents\PowerShell\Modules;
+   C:\Program Files\PowerShell\Modules;
+   c:\program files\powershell\7\Modules;
    C:\\Modules\az_12.4.0;
-   C:\Users\packer\Documents\WindowsPowerShell\Modules;
+   C:\Program Files\WindowsPowerShell\Modules;
+   C:\Windows\system32\WindowsPowerShell\v1.0\Modules;
    C:\Program Files\Microsoft SQL Server\130\Tools\PowerShell\Modules\

os.environ[USERPROFILE]=
    C:\Users\runneradmin

@mwichmann
Copy link
Collaborator

adaptive: possibly a good thing. the runners are weird overloaded environments "with everything" that probably don't match dev boxes. is the ps version chosen purely by First In PATH, or are there other ways to select?

@jcbrill
Copy link
Contributor Author

jcbrill commented May 15, 2025

is the ps version chosen purely by First In PATH, or are there other ways to select?

In the optional vcpkg.bat call chain, the first pwsh.exe (PS7) or powershell.exe (PS5) will be used.

In vsdevcmd.bat call chain, powershell.exe will be used even if PS7 precedes PS5 on the path due to different executable names. This is for telemetry data if VSCMD_SKIP_SENDTELEMETRY is not defined.

Both versions of powershell will use PSModulePath if defined or their own default search paths if not defined.

It may not be the worst idea to re-evaluate what is added to the Platform/win32 module as well.

For example, powershell 5 has been in the system path and PSModulePath has been defined since Windows Vista (i.e., post-XP).

Powershell 7 is not installed by default.

Windows Environments

The Platform/win32.py environment is crazy minimal for modern Windows:

'ENV': {
    'COMSPEC': 'C:\\Windows\\system32\\cmd.exe',
    'PATH': 'C:\\Windows\\System32',
    'PATHEXT': '.COM;.EXE;.BAT;.CMD',
    'SystemDrive': 'C:',
    'SystemRoot': 'C:\\Windows',
    'TEMP': 'C:\\Users\\jbrill\\AppData\\Local\\Temp',
    'TMP': 'C:\\Users\\jbrill\\AppData\\Local\\Temp',
    'USERPROFILE': 'C:\\Users\\myusername'
}

As mentioned previously ad nauseum, the default Platform/win32 and msvc environments lack the common Windows system environment variables:

  'CommonProgramFiles',
  'CommonProgramFiles(arm)',
  'CommonProgramFiles(x86)',
  'CommonProgramW6432',
  'PROCESSOR_ARCHITECTURE',
  'PROCESSOR_ARCHITEW6432',
  'ProgramData',
  'ProgramFiles',
  'ProgramFiles(arm)',
  'ProgramFiles(x86)',
  'ProgramW6432',
  'PSModulePath',

The following system environment variables are known to be referenced in the msvc 2022 batch files:

  • <VSROOT>/Common7/Tools/vsdevcmd/core/msbuild.bat
    • PROCESSOR_ARCHITECTURE
  • <VSROOT>Common7/Tools/vsdevcmd/ext/html_help.bat
    • ProgramFiles
    • ProgramFiles(x86)
  • <VSROOT>Common7/Tools/vsdevcmd/ext/netfxsdk.bat
    • PROCESSOR_ARCHITECTURE
  • <VSROOT>Common7/Tools/vsdevcmd/ext/vcvars/vcvars140.bat
    • ProgramFiles
    • ProgramFiles(x86)
  • <VSROOT>Common7/Tools/vsdevcmd/ext/vcvars
    • ProgramFiles
    • ProgramFiles(x86)
  • <VSROOT>Common7/Tools/VSDevCmd.bat
    • ProgramFiles(x86)
    • ProgramData INDIRECTLY

If one were to compare the paths generated from the command-line versus from SCons, one would find minor differences in the paths configured. To date, either it doesn't matter and/or users adjust the paths themselves after the fact.

It is likely that anyone attempting to run vswhere.exe in the limited msvc environment has to add ProgramData (MongoDB?).

It almost appears like the windows environments were written before windows supported 64-bit platforms and really never evolved.

Legend:

  • Normal text: default Platform/win32.py environment
  • + Green text: msvc environment
  • - Red text: not in wither platform/win32 or msvc environment

Default Windows 11, 10:

Path=
    C:\WINDOWS\system32;
-   C:\WINDOWS;
+   C:\WINDOWS\System32\Wbem;
+   C:\WINDOWS\System32\WindowsPowerShell\v1.0\;
-   C:\WINDOWS\System32\OpenSSH\;
-   C:\Users\%USERNAME%\AppData\Local\Microsoft\WindowsApps;

- PSModulePath=
-     C:\Program Files\WindowsPowerShell\Modules;
-     C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules

Default Windows 8, 7, Vista:

Path=
    C:\Windows\system32;
-   C:\Windows;
+   C:\Windows\System32\Wbem;
+   C:\Windows\System32\WindowsPowerShell\v1.0\

- PSModulePath=
-    C:\Windows\system32\WindowsPowerShell\v1.0\Modules\

Default Windows XP:

Path=
    C:\WINDOWS\system32;
-   C:\WINDOWS;
+   C:\WINDOWS\System32\Wbem

@jcbrill
Copy link
Contributor Author

jcbrill commented May 15, 2025

@mwichmann #4226 appears to avoid all of the above as subprocess is called directly to run vcpkg.exe and any vcpkg batch files, if necessary, without an explicit env argument (i.e., effectively using python process os.environ).

@mwichmann
Copy link
Collaborator

%ProcessorArchitecture% is looked at somewhere, sure I remember that, but I guess it's not written to ENV.

Based on what you say, I wonder if using a restricted environment for the setup run is the right choice. After all, we do want to accurately detect what's available. Then, of course, we'd have to figure out how many more things have to be exported to the build-time ENV.

@mwichmann
Copy link
Collaborator

mwichmann commented May 15, 2025

Looking at the vcpkg stuff, I guess it's this stanza?

:: Call powershell which may or may not invoke bootstrap if there's a version mismatch
SET Z_POWERSHELL_EXE=
FOR %%i IN (pwsh.exe powershell.exe) DO (
  IF EXIST "%%~$PATH:i" (
    SET "Z_POWERSHELL_EXE=%%~$PATH:i"
    GOTO :gotpwsh
  )
)

:gotpwsh
"%Z_POWERSHELL_EXE%" -NoProfile -ExecutionPolicy Unrestricted -Command "iex (get-content \"%~dfp0\" -raw)#"

Since the temporary variable with the powershell location is unset immediately afterward and not further used. And the expression evaluation seems to just be path fiddling? So how does this manage to go off in the weeds and take a long time? Do I even want to know (well, "want" == No here, but anyway...)

@mwichmann
Copy link
Collaborator

mwichmann commented May 15, 2025

@mwichmann #4226 appears to avoid all of the above as subprocess is called directly to run vcpkg.exe and any vcpkg batch files, if necessary, without an explicit env argument (i.e., effectively using python process os.environ).

I suspect if that PR were to be pushed forward, someone (probably me) would have suggested changing it to use scons_subproc_run, and thus reintroducing the limited environment... I'll keep this in mind if it ever goes anywhere.

@jcbrill
Copy link
Contributor Author

jcbrill commented May 15, 2025

All good questions.

Blaise Pascal (Letter 16, 1657):

“I have only made this letter longer because I have not had the time to make it shorter."

Apologies for the verbose responses that follow.

Based on what you say, I wonder if using a restricted environment for the setup run is the right choice. After all, we do want to accurately detect what's available. Then, of course, we'd have to figure out how many more things have to be exported to the build-time ENV.

That is the dilemma in a nutshell.

This is of course anathema to purists that do not want any possibility of tight coupling to an end user's environments. On the other hand, running in an OS-like environment probably removes a considerable maintenance/support burden.

For preserved variables, the KEEPLIST is a module-level variable which could be monkey-patched in an emergency by a user.

What a user cannot do right now is set the environment used to invoke the msvc batch files. Using the os.environ would eliminate the need to import variables into the environment and manually set the system path elements.

With the VS VCPKG component, we would likely have to VCPKG_ROOT and VCPKG_DISABLE_METRICS which are currently imported into the msvc environment but not preserved.

While this is less of problem today, keep in mind that the command shell environment could be 32-bit, and/or the python architecture could be 32-bit on a Windows 64-bit host. Running the environment set by running the vcvars batch file would be subtly different than running the same batch files from a 64-bit shell and 64-bit build of python. This effects the system variables.

I once used a Windows third-party toolbar to launch a command shell. The tool was 32-bit so the default launched environment was configured for 32-bit applications.

I believe that the vcvars batch files should be run in the same environment native to the host. This would require more fiddling and explicitly launching the vcvars batch files via a fully qualified path to the command interpreter due to WoW redirection.

On ARM64, there is a subtle difference in vcvars results between running an amd64 build of python versus an ARM64 build of python due to PROCESSOR_ARCHITECTURE. The latest toolsets for VS2022 have native ARM64 builds.

While unlikely, on ARM64, python could be 32-bit arm, 32-bit x86, 64-bit amd64, or 64-bit ARM64. I believe there are 3 shell environment (I could be wrong): 32-bit x86, 64-bit amd64, or 64-bit ARM64.

It is almost trivial to run the one-time vcvars batch files with an explicit command interpreter if necessary.

I suspect if that PR were to be pushed forward, someone (probably me) would have suggested changing it to use scons_subproc_run, and thus reintroducing the limited environment... I'll keep this in mind if it ever goes anywhere.

Bit of trivia: the vswhere query in MSCommon/vc.py uses subprocess directly. vswhere.exe needs ProgramData to be defined (or at least it used to).

Passing env=dict(os.environ) to scons_subproc_run(scons_env, *args, **kwargs) should use the system environment which should be equivalent to subprocess.run(...env=None).

Since the temporary variable with the powershell location is unset immediately afterward and not further used. And the expression evaluation seems to just be path fiddling? So how does this manage to go off in the weeds and take a long time? Do I even want to know (well, "want" == No here, but anyway...)

My guess is that when calling itself as a powershell script, there is delay is introduced in one or more of the following: determining the default PSModulePath, loading powershell modules, downloading and "installing" information from github. Somewhere in the snippet below (alas some code removed) lies the root cause of the delay.

Rough call sequence:

  • vcpkg.bat is calling vcpgk-init.bat
  • vcpkg-init.cmd calls itself as a powershell script
    • "%Z_POWERSHELL_EXE%" -NoProfile -ExecutionPolicy Unrestricted -Command "iex (get-content \"%~dfp0\" -raw)#"
  • powershell vcpkg-init.cmd script:
    ...
    
    function download($url, $path) {
      $wc = New-Object net.webclient
      Write-Host "Downloading '$url' -> '$path'"
      $wc.DownloadFile($url, $path);
      $wc.Dispose();
      if( (get-item $path).Length -ne $wc.ResponseHeaders['Content-Length'] ) {
        throw "Download of '$url' failed.  Check your internet connection."
      }
      if (-Not $IsWindows) {
        chmod +x $path
      }
    
      return $path
    }
    
    ...
    
    function bootstrap-vcpkg {
      if (-Not ($VCPKG_INIT_VERSION -eq 'latest') `
        -And (Test-Path $VCPKG_VERSION_MARKER) `
        -And ((Get-Content -Path $VCPKG_VERSION_MARKER -Raw).Trim() -eq $VCPKG_INIT_VERSION)) {
            return $True
      }
    
      Write-Host "Installing vcpkg to $env:VCPKG_ROOT"
      New-Item -ItemType Directory -Force $env:VCPKG_ROOT | Out-Null
    
      if ($IsWindows) {
        download https://github.com/microsoft/vcpkg-tool/releases/download/2024-12-09/vcpkg.exe $VCPKG
      } elseif ($IsMacOS) {
        download https://github.com/microsoft/vcpkg-tool/releases/download/2024-12-09/vcpkg-macos $VCPKG
      } elseif (Test-Path '/etc/alpine-release') {
        download https://github.com/microsoft/vcpkg-tool/releases/download/2024-12-09/vcpkg-muslc $VCPKG
      } else {
        download https://github.com/microsoft/vcpkg-tool/releases/download/2024-12-09/vcpkg-glibc $VCPKG
      }
    
      & $VCPKG bootstrap-standalone
      if(-Not $?) {
        Write-Error "Bootstrap failed."
        return $False
      }
    
      Write-Host "Bootstrapped vcpkg: $env:VCPKG_ROOT"
      return $True
    }
    
    if(-Not (bootstrap-vcpkg)) {
      throw "Unable to install vcpkg."
    }
    
    # Export vcpkg to the current shell.
    New-Module -name vcpkg -ArgumentList @($VCPKG) -ScriptBlock {
      param($VCPKG)
      function vcpkg-shell() {
        # setup the postscript file
        # Generate 31 bits of randomness, to avoid clashing with concurrent executions.
        $env:Z_VCPKG_POSTSCRIPT = Join-Path ([System.IO.Path]::GetTempPath()) "VCPKG_tmp_$(Get-Random -SetSeed $PID).ps1"
        & $VCPKG @args
        # dot-source the postscript file to modify the environment
        if (Test-Path $env:Z_VCPKG_POSTSCRIPT) {
          $postscr = Get-Content -Raw $env:Z_VCPKG_POSTSCRIPT
          if( $postscr ) {
            iex $postscr
          }
    
          Remove-Item -Force -ea 0 $env:Z_VCPKG_POSTSCRIPT
        }
    
        Remove-Item env:Z_VCPKG_POSTSCRIPT
      }
    } | Out-Null
    
    ...
    

@jcbrill
Copy link
Contributor Author

jcbrill commented May 15, 2025

Forgot to add: an SCons env['ENV'] can be substantially and materially different from the environment in which the msvc tools are run.

@mwichmann
Copy link
Collaborator

Forgot to add: an SCons env['ENV'] can be substantially and materially different from the environment in which the msvc tools are run.

Not sure I quite get this comment. If you open a "developer command prompt" (cmd or ps variant) you get a shell with a boatload of environment variables set. Of course that's different to what we run the build commands with, given the scons policy we all know and love. That doesn't seem likely to have been your point?

@jcbrill
Copy link
Contributor Author

jcbrill commented May 16, 2025

Not sure I quite get this comment. If you open a "developer command prompt" (cmd or ps variant) you get a shell with a boatload of environment variables set. Of course that's different to what we run the build commands with, given the scons policy we all know and love. That doesn't seem likely to have been your point

I did a poor job trying to provide a reminder that in the current code, the environment passed to the subprocess invocation of the msvc batch files ignores everything in the user's env['ENV'].

MSCommon/common.py

def get_output(vcbat, args=None, env=None, skip_sendtelemetry=False):
    ...
    if env is None:
        # Create a blank environment, for use in launching the tools
        env = SCons.Environment.Environment(tools=[])
    ...

MSCommon/vc.py

def script_env(env, script, args=None):
    ...
    if cache_data is None:
        skip_sendtelemetry = _skip_sendtelemetry(env)
        stdout = common.get_output(script, args, skip_sendtelemetry=skip_sendtelemetry)
    ...

The get_output invocation from MSCommon/vc.py is not passed an env argument. Inside get_output, a new environment is materialized if the env argument is None.

It does not matter what is in the user's env['ENV']. It won't be used at all. In this respect, the msvc tool initialization may be very different than other tools.

That is why suggestions like the following aren't going to work:

env = Environment(tools=[])
env['ENV']['PSModulePath'] = os.environ['PSModulePath']
env.Tool('msvc')
env.Tool('mslink')

The environment in which the msvc batch files is invoked won't contain the PSModulePath variable and value.

RELEASE.txt Outdated
@@ -35,7 +35,7 @@ CHANGED/ENHANCED EXISTING FUNCTIONALITY
one from PEP 308 introduced in Python 2.5 (2006). The idiom being
replaced (using and/or) is regarded as error prone.

- MSVS: The default Windows powershell 7 path is added before the default
- MSVC: The default Windows powershell 7 path is added before the default
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs a blurb about the user impact. Perhaps something like.

"In some environments (such as Github Actions) this can yield a significant reduction in the time to initialize MSVC. (Without this change you may see > 10x slowdown) "

replace ##x slowdown with your best approximation of your test results.

The idea is that anyone reading this looking for a version with a fix should know from this blurb that it is indeed fixed.

RELEASE.txt is what gets sent to the mailing lists and other notification channels. Users then would have to go to the CHANGES.txt.

So RELEASE.txt should have more or less the same content as this releases CHANGE.txt blurb, but organized differently, without the attribution.

@jcbrill
Copy link
Contributor Author

jcbrill commented May 17, 2025

Prototype "adaptive" powershell results.

Changes:

  • Powershell 7 will be added to the msvc environment path if pwsh.exe is found on the system path (os.environ['PATH']).
  • Powershell 5 will be added to the msvc environment path as is done currently.
  • Powershell 7 (if found) and Powershell 5 will be added to the msvc environment path in the order found on the system path.
  • Known paths (PS 7 and PS 5 Current User, All Users, and Install location paths) from the system PSModulePath (i.e., os.environ['PSModulePath']) will be added to the msvc environment PSModulePath in the order found on the system PSModulePath.
  • PSModulePath is only added to the msvc environment if at least one element is present.

Results from two environments are shown below:

  • Windows runner (PS 7 and PS 5)
  • Local environment (PS 5 only)

I think this is way it should be done. The only hesitation is in including the Current User powershell paths. In this limited use context, I don't think it matters and would be similar to running in the OS environment.

Note: Not all of the PSModulePath elements are passed to the msvc environment. Only the elements that comprise the powershell "default paths" if PSModulePath is undefined.

This does "fix" the runtime issue in the windows runner.

Prototype Results

Diff code language used for highlighting purposes.

Windows runner (PS 7 & PS 5):

os.environ[PATH]=
+   C:\Program Files\PowerShell\7;
    C:\Program Files\MongoDB\Server\5.0\bin;
    C:\aliyun-cli;
    C:\vcpkg;
    C:\Program Files (x86)\NSIS\;
    C:\tools\zstd;
    C:\Program Files\Mercurial\;
    C:\hostedtoolcache\windows\stack\3.5.1\x64;
    C:\cabal\bin;
    C:\\ghcup\bin;
    C:\mingw64\bin;
    C:\Program Files\dotnet;
    C:\Program Files\MySQL\MySQL Server 8.0\bin;
    C:\Program Files\R\R-4.4.2\bin\x64;
    C:\SeleniumWebDrivers\GeckoDriver;
    C:\SeleniumWebDrivers\EdgeDriver\;
    C:\SeleniumWebDrivers\ChromeDriver;
    C:\Program Files (x86)\sbt\bin;
    C:\Program Files (x86)\GitHub CLI;
    C:\Program Files\Git\bin;
    C:\Program Files (x86)\pipx_bin;
    C:\npm\prefix;
    C:\hostedtoolcache\windows\go\1.24.3\x64\bin;
    C:\hostedtoolcache\windows\Python\3.9.13\x64\Scripts;
    C:\hostedtoolcache\windows\Python\3.9.13\x64;
    C:\hostedtoolcache\windows\Ruby\3.0.7\x64\bin;
    C:\Program Files\OpenSSL\bin;
    C:\tools\kotlinc\bin;
    C:\hostedtoolcache\windows\Java_Temurin-Hotspot_jdk\8.0.452-9\x64\bin;
    C:\Program Files\ImageMagick-7.1.1-Q16-HDRI;
    C:\Program Files\Microsoft SDKs\Azure\CLI2\wbin;
    C:\ProgramData\kind;
    C:\ProgramData\Chocolatey\bin;
    C:\Windows\system32;
    C:\Windows;
    C:\Windows\System32\Wbem;
+   C:\Windows\System32\WindowsPowerShell\v1.0\;
    C:\Windows\System32\OpenSSH\;
    C:\Program Files\dotnet\;
+   C:\Program Files\PowerShell\7\;
    C:\Program Files\Microsoft\Web Platform Installer\;
    C:\Program Files\TortoiseSVN\bin;
    C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\;
    C:\Program Files\Microsoft SQL Server\150\Tools\Binn\;
    C:\Program Files (x86)\Windows Kits\10\Windows Performance Toolkit\;
    C:\Program Files (x86)\WiX Toolset v3.14\bin;
    C:\Program Files\Microsoft SQL Server\130\DTS\Binn\;
    C:\Program Files\Microsoft SQL Server\140\DTS\Binn\;
    C:\Program Files\Microsoft SQL Server\150\DTS\Binn\;
    C:\Program Files\Microsoft SQL Server\160\DTS\Binn\;
    C:\Strawberry\c\bin;
    C:\Strawberry\perl\site\bin;
    C:\Strawberry\perl\bin;
    C:\ProgramData\chocolatey\lib\pulumi\tools\Pulumi\bin;
    C:\Program Files\CMake\bin;
    C:\ProgramData\chocolatey\lib\maven\apache-maven-3.9.9\bin;
    C:\Program Files\Microsoft Service Fabric\bin\Fabric\Fabric.Code;
    C:\Program Files\Microsoft SDKs\Service Fabric\Tools\ServiceFabricLocalClusterManager;
    C:\Program Files\nodejs\;
    C:\Program Files\Git\cmd;
    C:\Program Files\Git\mingw64\bin;
    C:\Program Files\Git\usr\bin;
    C:\Program Files\GitHub CLI\;
    c:\tools\php;
    C:\Program Files (x86)\sbt\bin;
    C:\Program Files\Amazon\AWSCLIV2\;
    C:\Program Files\Amazon\SessionManagerPlugin\bin\;
    C:\Program Files\Amazon\AWSSAMCLI\bin\;
    C:\Program Files\Microsoft SQL Server\130\Tools\Binn\;
    C:\Program Files\LLVM\bin;
    C:\Users\%USERNAME%\.dotnet\tools;
    C:\Users\%USERNAME%\.cargo\bin;
    C:\Users\%USERNAME%\AppData\Local\Microsoft\WindowsApps

os.environ[PSMODULEPATH]=
+   C:\Users\%USERNAME%\Documents\PowerShell\Modules;
+   C:\Program Files\PowerShell\Modules;
+   c:\program files\powershell\7\Modules;
    C:\\Modules\az_12.4.0;
-   C:\Users\packer\Documents\WindowsPowerShell\Modules;
+   C:\Program Files\WindowsPowerShell\Modules;
+   C:\Windows\system32\WindowsPowerShell\v1.0\Modules;
    C:\Program Files\Microsoft SQL Server\130\Tools\PowerShell\Modules\

env[PATH]=
    C:\Windows\System32;
    C:\Windows\System32\Wbem;
+   C:\Program Files\PowerShell\7;
    C:\Windows\System32\WindowsPowerShell\v1.0\

+ env[PSModulePath]=
+    C:\Users\%USERNAME%\Documents\PowerShell\Modules;
+    C:\Program Files\PowerShell\Modules;
+    C:\Program Files\PowerShell\7\Modules;
+    C:\Program Files\WindowsPowerShell\Modules;
+    C:\Windows\System32\WindowsPowerShell\v1.0\Modules

Local machine (PS 5):

os.environ[PATH]=
    C:\Data\venv\scons-dev\3.11.9\Scripts;
    C:\Program Files (x86)\Common Files\Intel\Shared Libraries\redist\intel64_win\compiler;
    C:\Program Files (x86)\Common Files\Intel\Shared Libraries\redist\ia32_win\compiler;
    C:\Windows\system32;
    C:\Windows;
    C:\Windows\System32\Wbem;
+   C:\Windows\System32\WindowsPowerShell\v1.0\;
    C:\Windows\System32\OpenSSH\;
    C:\Program Files\dotnet\;
    C:\Software\PuTTY\;
    C:\Program Files\Microsoft SQL Server\110\Tools\Binn\;
    C:\Program Files\Microsoft SQL Server\120\Tools\Binn\;
    C:\Program Files (x86)\Microsoft SDKs\TypeScript\1.0\;
    C:\Users\%USERNAME%\.dnx\bin;
    C:\Program Files\Microsoft DNX\Dnvm\;
    C:\Program Files\Microsoft SQL Server\130\Tools\Binn\;
    C:\Program Files\Microsoft\Web Platform Installer\;
    C:\Software\TortoiseHg\;
    C:\Software\Git\cmd;
    C:\Program Files (x86)\Windows Kits\10\Windows Performance Toolkit\;
    C:\Software\TortoiseGit\bin;
    C:\Users\%USERNAME%\.pyenv\pyenv-win\bin;
    C:\Users\%USERNAME%\AppData\Local\Microsoft\WindowsApps;
    C:\Users\%USERNAME%\.dotnet\tools;
    C:\Users\%USERNAME%\AppData\Local\GitHubDesktop\bin;
    C:\Users\%USERNAME%\AppData\Roaming\npm;
    C:\Users\%USERNAME%\AppData\Local\JpegminiPro4\;
    C:\Users\%USERNAME%\AppData\Local\Markdown Monster

os.environ[PSMODULEPATH]=
+   C:\Program Files\WindowsPowerShell\Modules;
+   C:\Windows\system32\WindowsPowerShell\v1.0\Modules

env[PATH]=
    C:\Windows\System32;
    C:\Windows\System32\Wbem;
    C:\Windows\System32\WindowsPowerShell\v1.0\

+ env[PSModulePath]=
+     C:\Program Files\WindowsPowerShell\Modules;
+     C:\Windows\System32\WindowsPowerShell\v1.0\Modules

@jcbrill
Copy link
Contributor Author

jcbrill commented May 18, 2025

Adaptive powershell path elements and PSModulePath handling in MSCommon/common.py branch of this PR:
jcbrill@061fec7

If desired, this PR could be updated with the changes from the branch.

Commit message:

Update PATH and PSModulePath handling for msvc environment in MSCommon/common.py.

Changes:

  • Replace text search of compound path string with normalized path (normpath and normcase) text in list of existing elements. Previous method was a case-sensitive search and could erroneously not add a path that happened to be a proper prefix of an existing path (e.g., "C:\Windows" would not added if "C:\Windows\system32" was already in path).
  • Add the powershell 7 and powershell 5 executable paths to the msvc environment system path in the order in which they appear on the OS system path. Force the powershell 5 path if necessary.
  • Add the known powershell module locations (i.e., Current User, All User, and install path) for powershell 7 and powershell 5 to the msvc environment PSModulePath in the order in which they appear on the OS system PSModulePath.
  • Add msvc vcvars batch elapsed time to log as a convenience.

Additional known "challenges" with the existing MSCommon/common and MSCommon/vc code should probably be documented.

Comments and questions welcome.

PS: I am time challenged for the next 7 days.

@jcbrill
Copy link
Contributor Author

jcbrill commented May 18, 2025

The vcpkg search for the powershell executable will use pwsh.exe (PS 7) if it is anywhere on the path; otherwise, it will use powershell.exe (PS 5) if it is on the path.

The path order does not matter: PS 7 will be used even if the PS 5 path precedes the PS 7 path.

Sigh.

@mwichmann
Copy link
Collaborator

That's what I see in the code snip above - it's a loop which exits on first match.

@jcbrill
Copy link
Contributor Author

jcbrill commented May 18, 2025

Yep.

The logic in the commit above is still the way to go.

I have updated and committed MSCommon/common.py in the other branch with a revised comment: jcbrill@43270cb

# Add the powershell v7 and v5 executable paths in the order found on
# the system path.
# The order of the powershell paths for the VS vcpkg does not matter.
# The system path is searched for pwsh.exe (v7) first and then 
# powershell.exe (v5).

@jcbrill
Copy link
Contributor Author

jcbrill commented May 18, 2025

adaptive: possibly a good thing. the runners are weird overloaded environments "with everything" that probably don't match dev boxes. is the ps version chosen purely by First In PATH, or are there other ways to select?

Corrected answer.

vcvars Telemetry:

  • powershell.exe (PS 5)

VS vcpkg powershell executable priority (path order does not matter):

  1. pwsh.exe (PS 7) if on system PATH
  2. powershell.exe (PS 7) if on system PATH

@mwichmann
Copy link
Collaborator

So given how I now understand this information, here are two possible rewordings of the changelog snip - one verbose (I have this tendency too :-) ), and one very concise in a just-the-facts-without-details form. Just tossing these out there, feel free to ignore these, or meld them into a new version, or whatever.

  • MSVC: unexpectedly poor performance was reported using SCons on the
    Windows 2025 GitHub Actions runner, which was recently promoted to be
    'windows-latest'. Upon invenstigation, it was found that two factors
    combined to account for much of the slowdown: Powershell 7 (or more
    specifically, the binary pwsh.exe, a different name from the binary
    for earlier versions, powershell.exe) is installed, and the vcpkg
    Visual Studion compounent is installed. On the GitHub runners, this
    is the case, as they are provisioned with everything developers can
    be expected to need. During initialization of the SCons msvc tool, if
    msvc config caching is not enabled (or if this is a "first run" as is
    likely the case for a fresh CI runner image), the MSVC initialization
    batch script is run, which will find the vcpkg initialization script
    and run it, which will select pwsh.exe if found, which leads to the
    problem path. THe mitigation is to change the environment in which the
    MSVC batch script runs so Powershell 7 finds the right set of scripts:
    the shell environment variables VCPKG_DISABLE_METRICS and VCPKG_ROOT
    are added. Note that these two environment variables are not presently
    propagated to the build environment.

  • MSVC: fixed performance regression on Windows 2022/2025 GitHub
    Actions runners. The issue occurred when both PowerShell 7 (pwsh.exe)
    and the Visual Studio vcpkg component were present, as is the case for
    the default GitHub runners, causing slow MSVC tool initialization.
    The fix sets the VCPKG_DISABLE_METRICS and VCPKG_ROOT environment
    variables during MSVC batch script execution to avoid vcpkg
    initialization overhead. Note: these variables are only used during
    tool initialization and are not propagated to the build environment.

@jcbrill
Copy link
Contributor Author

jcbrill commented May 19, 2025

unexpectedly poor performance was reported using SCons on the
Windows 2025 GitHub Actions runner, which was recently promoted to be
'windows-latest'.

Isn't windows-2022 still windows-latest?

@mwichmann
Copy link
Collaborator

yes, you're right. I thought I had read it had flipped, but -latest does still point to 2022.

@jcbrill
Copy link
Contributor Author

jcbrill commented May 19, 2025

New theory under investigation: a non-trivial component of the slowdown might be the first time powershell.exe is invoked and not necessarily the VS vcpkg initialization. The VS vcpkg initialization could trigger the first powershell invocation depending on the value of VSCMD_SKIP_SENDTELEMETRY value.

@mwichmann
Copy link
Collaborator

The other way around: powershell.exe without PSModulePath defined is the problem. Adding pwsh.exe to the path is a solution.

The following applies to the windows 2022/2025 runners. VS vcpkg not installed in windows 2019 runner.

powershell.exe without PSModulePath and powershell.exe with PSModulePath containing only PS 5 modules are the problem.

I get re-confused every go-round of this. So the old powershell causes problems, but that's the situation we've always had? I do see that standalone vcpkg has been installed on the runner since at least the 2019 image, but only 2022 and 2025 have the vcpkg VS component Microsoft.VisualStudio.Component.Vcpkg. So the conclusion is the addition of a "findable" vcpkg is the trigger? Perhaps that's why the vcpkg script is coded to find PS7 first, then?

@jcbrill
Copy link
Contributor Author

jcbrill commented May 19, 2025

I get re-confused every go-round of this. So the old powershell causes problems, but that's the situation we've always had?

Yes. Old powershell without PSModulePath (important).

I believe that the first invocation of powershell creates a command cache. Subsequent invocations may be faster.

To date, all of my tests have VSCMD_SKIP_SENDTELEMETRY=1, therefore:
2019: powershell never invoked
2022: powershell invoked due to vcpkg
2025: powershell invoked due to vcpkg

I may run some tests without VSCMD_SKIP_SENDTELEMETRY to see if there is a runtime penalty in 2019 as well. It is possible there may have been a runtime penalty in all environments that has gone undetected until now. For example, if it takes a long time to build your project, 30 seconds might go unnoticed.

Unfortunately, these types of issues could lead to the conclusion that "SCons is slow" mostly due to the restricted msvc environment in which the msvc commands are run.

Running a powershell.exe command to list the available modules ('"powershell.exe" Get-Module -ListAvailable') is slow without PSModulePath defined:
2019: PS=(43.26s, 20.55s) MSVC=( 1.41s, 0.62s)
2022: PS=(87.08s, 24.47s) MSVC=(23.58s, 21.10s)
2025: PS=(52.58s, 27.72s) MSVC=(22.28s, 21.81s)

The list of available modules is very long. I have a few more experiments to try that may shed some light on the differences between PS 5 with and without PSModulePath and PS 7 with and without PSModulePath.

This of course could be a red herring.

I do see that standalone vcpkg has been installed on the runner since at least the 2019 image, but only 2022 and 2025 have the vcpkg VS component Microsoft.VisualStudio.Component.Vcpkg. So the conclusion is the addition of a "findable" vcpkg is the trigger? Perhaps that's why the vcpkg script is coded to find PS7 first, then?

The aggressively minimal msvc environment is [was] the issue:

  • Running powershell.exe from the command-line with the OS definition of PSModulePath is fast.
  • Running powershell.exe inside the msvc environment without PSModulePath is slow.

I'm still working on the differences between with and without PSModulePath for powershell.exe and pwsh.exe.

My guess is that the idea is to use the latest and greatest if possible.

@mwichmann
Copy link
Collaborator

mwichmann commented May 20, 2025

a thought... apparently, a famous powershell slowdown is Optimizing (.net) Assemblies after a .net update. if it doesn't have its path, no chance of picking up ones that are already done in the image?

@jcbrill
Copy link
Contributor Author

jcbrill commented May 20, 2025

a thought... apparently, a famous powershell slowdown is Optimizing (.net) Assemblies after a .net update. if it doesn't have its path, no chance of picking up ones that are already done in the image?

I don't know.

I believe the first time powershell/pwsh is invoked it may build a cache of available commands. Under certain circumstances, this is really expensive for powershell.exe and not as bad as with pwsh.exe. I'm not sure what building the cache entails.

It appears like when PSModulePath is not defined, both powershell.exe and pwsh.exe retrieve the registry value of PSModulePath. However, powershell.exe appears to ignore the PS7 path components. This is not the same as defining PSModulePath to the OS value.

With VSCMD_SKIP_SENDTELEMETRY defined, the VS vcpkg initialization will be the first powershell.exe/pwsh.exe invocation. I'm leaning towards there is a penalty experienced due to building the powershell module cache.

Putting finishing touches on some experiments to show what may be happening. If it is the initial call, it is kind of a Heisenberg challenge to prove.

@jcbrill
Copy link
Contributor Author

jcbrill commented May 20, 2025

I believe there are two initialization penalties:

  1. when powershell needs to initialize/update the powershell module cache
  2. when VS vcpkg is called the first time

The magnitude of the penalty for 1 is based on how the restricted msvc environment is configured. The magnitude of the penalty for 2 is based on how the restricted msvc enironment is configured and external factors.

It may be the case that both 1 and 2 are experienced on the first VS vcpkg batch file invocation.

To test this theory, the powershell executables are called using the restricted msvc environment.

The command executed is gci env: | Out-String -Width 4096 which simply displays the environment variables. This would appear to be enough to trigger module loading.

The powershell.exe and/or pwsh.exe executables are invoked three (3) times consecutively.

The msvc vcvars batch file is then called three (3) times consecutively.

The first invocation of powershell compared to subsequent invocations appears to imply an initialization "setup cost".

The first invocation of the msvc vcvars batch files appears to imply an intialization "setup cost" due to vcpkg initializing.

The magnitude of the elapsed times can be wildly inconsistent. For example, in Run 1 below, the first powershell invocation took 74.23 seconds. Twenty minutes earlier I think it tool 34 seconds in one experiment.

For traditional SCons builds, the powershell and vcpkg initialization costs are hidden within the msvc batch file invocation time. For the experiments below, the powershell first time cost is outside of the msvc batch file invocation time leaving only the vcpkg first time cost.

Run 1 below indicates that even after the initial powershell cost, all powershell invocations are "expensive" compared to the other configurations.

Run 1 is how the restricted msvc environment was configured prior to adding the PS 7 executable path.

When PSModulePath is undefined, the powershell executables appear to inspect the PSModulePath definition in the registry.

Run 1 - windows 2022, powershell.exe only, PSModulePath undefine

os.environ[PSMODULEPATH]=
    C:\Users\runneradmin\Documents\PowerShell\Modules
    C:\Program Files\PowerShell\Modules
    c:\program files\powershell\7\Modules
    C:\\Modules\az_12.4.0
    C:\Users\packer\Documents\WindowsPowerShell\Modules
    C:\Program Files\WindowsPowerShell\Modules
    C:\Windows\system32\WindowsPowerShell\v1.0\Modules
    C:\Program Files\Microsoft SQL Server\130\Tools\PowerShell\Modules\

CONFIG = PATH_5_MODULEPATH_NA

env[PATH]=
    C:\Windows\System32;
    C:\Windows\System32\Wbem;
    C:\Windows\System32\WindowsPowerShell\v1.0\

env[PSModulePath]=

CALLING='powershell.exe gci env: | Out-String -Width 4096'

stdout:PSMODULEPATH=
    C:\Users\runneradmin\Documents\WindowsPowerShell\Modules
    C:\\Modules\az_12.4.0
    C:\Users\packer\Documents\WindowsPowerShell\Modules
    C:\Program Files\WindowsPowerShell\Modules
    C:\Windows\system32\WindowsPowerShell\v1.0\Modules
    C:\Program Files\Microsoft SQL Server\130\Tools\PowerShell\Modules\

ELAPSED_TIME=74.23, cmd='powershell.exe gci env: | Out-String -Width 4096'
ELAPSED_TIME=20.51, cmd='powershell.exe gci env: | Out-String -Width 4096'
ELAPSED_TIME=20.39, cmd='powershell.exe gci env: | Out-String -Width 4096'

ELAPSED_TIME=24.95, vc_script='C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise\\VC\\Auxiliary\\Build\\vcvars64.bat'
ELAPSED_TIME=20.97, vc_script='C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise\\VC\\Auxiliary\\Build\\vcvars64.bat'
ELAPSED_TIME=22.44, vc_script='C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise\\VC\\Auxiliary\\Build\\vcvars64.bat'

Run 2 - windows 2022, powershell.exe only, OS PSModulePath

os.environ[PSMODULEPATH]=
    C:\Users\runneradmin\Documents\PowerShell\Modules
    C:\Program Files\PowerShell\Modules
    c:\program files\powershell\7\Modules
    C:\\Modules\az_12.4.0
    C:\Users\packer\Documents\WindowsPowerShell\Modules
    C:\Program Files\WindowsPowerShell\Modules
    C:\Windows\system32\WindowsPowerShell\v1.0\Modules
    C:\Program Files\Microsoft SQL Server\130\Tools\PowerShell\Modules\

CONFIG = PATH_5_MODULEPATH_OS

env[PATH]=
    C:\Windows\System32
    C:\Windows\System32\Wbem
    C:\Windows\System32\WindowsPowerShell\v1.0\

env[PSModulePath]=
    C:\Users\runneradmin\Documents\PowerShell\Modules
    C:\Program Files\PowerShell\Modules
    c:\program files\powershell\7\Modules
    C:\\Modules\az_12.4.0
    C:\Users\packer\Documents\WindowsPowerShell\Modules
    C:\Program Files\WindowsPowerShell\Modules
    C:\Windows\system32\WindowsPowerShell\v1.0\Modules
    C:\Program Files\Microsoft SQL Server\130\Tools\PowerShell\Modules\

CALLING='powershell.exe gci env: | Out-String -Width 4096'

stdout:PSModulePath
    C:\Users\runneradmin\Documents\PowerShell\Modules
    C:\Program Files\PowerShell\Modules
    c:\program files\powershell\7\Modules
    C:\\Modules\az_12.4.0
    C:\Users\packer\Documents\WindowsPowerShell\Modules
    C:\Program Files\WindowsPowerShell\Modules
    C:\Windows\system32\WindowsPowerShell\v1.0\Modules
    C:\Program Files\Microsoft SQL Server\130\Tools\PowerShell\Modules\

ELAPSED_TIME=0.39, cmd='powershell.exe gci env: | Out-String -Width 4096'
ELAPSED_TIME=0.29, cmd='powershell.exe gci env: | Out-String -Width 4096'
ELAPSED_TIME=0.30, cmd='powershell.exe gci env: | Out-String -Width 4096'

ELAPSED_TIME=5.22, vc_script='C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise\\VC\\Auxiliary\\Build\\vcvars64.bat'
ELAPSED_TIME=1.02, vc_script='C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise\\VC\\Auxiliary\\Build\\vcvars64.bat'
ELAPSED_TIME=0.98, vc_script='C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise\\VC\\Auxiliary\\Build\\vcvars64.bat'

Run 3 - windows 2022, pwsh.exe and powershell.exe, PSModulePath = known PS 7 and PS 5 module paths

os.environ[PSMODULEPATH]=
    C:\Users\runneradmin\Documents\PowerShell\Modules
    C:\Program Files\PowerShell\Modules
    c:\program files\powershell\7\Modules
    C:\\Modules\az_12.4.0
    C:\Users\packer\Documents\WindowsPowerShell\Modules
    C:\Program Files\WindowsPowerShell\Modules
    C:\Windows\system32\WindowsPowerShell\v1.0\Modules
    C:\Program Files\Microsoft SQL Server\130\Tools\PowerShell\Modules\

CONFIG = PATH_7_MODULEPATH_75

env[PATH]=
    C:\Windows\System32
    C:\Windows\System32\Wbem
    C:\Program Files\PowerShell\7
    C:\Windows\System32\WindowsPowerShell\v1.0\

env[PSModulePath]=
    C:\Users\runneradmin\Documents\PowerShell\Modules
    C:\Program Files\PowerShell\Modules
    C:\Program Files\PowerShell\7\Modules
    C:\Program Files\WindowsPowerShell\Modules
    C:\Windows\system32\WindowsPowerShell\v1.0\Modules

CALLING='pwsh.exe -Command "gci env: | Out-String -Width 4096"'

stdout:PSModulePath
    C:\Users\runneradmin\Documents\PowerShell\Modules
    C:\Program Files\PowerShell\Modules
    C:\Program Files\PowerShell\7\Modules
    C:\Program Files\WindowsPowerShell\Modules
    C:\Windows\system32\WindowsPowerShell\v1.0\Modules

ELAPSED_TIME=0.91, cmd='pwsh.exe -Command "gci env: | Out-String -Width 4096"'
ELAPSED_TIME=0.44, cmd='pwsh.exe -Command "gci env: | Out-String -Width 4096"'
ELAPSED_TIME=0.44, cmd='pwsh.exe -Command "gci env: | Out-String -Width 4096"'

CALLING='powershell.exe gci env: | Out-String -Width 4096'

stdout:PSModulePath
    C:\Users\runneradmin\Documents\PowerShell\Modules
    C:\Program Files\PowerShell\Modules
    C:\Program Files\PowerShell\7\Modules
    C:\Program Files\WindowsPowerShell\Modules
    C:\Windows\system32\WindowsPowerShell\v1.0\Modules

ELAPSED_TIME=0.33, cmd='powershell.exe gci env: | Out-String -Width 4096'
ELAPSED_TIME=0.31, cmd='powershell.exe gci env: | Out-String -Width 4096'
ELAPSED_TIME=0.31, cmd='powershell.exe gci env: | Out-String -Width 4096'

ELAPSED_TIME=3.22, vc_script='C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise\\VC\\Auxiliary\\Build\\vcvars64.bat'
ELAPSED_TIME=1.19, vc_script='C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise\\VC\\Auxiliary\\Build\\vcvars64.bat'
ELAPSED_TIME=1.20, vc_script='C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise\\VC\\Auxiliary\\Build\\vcvars64.bat'

@jcbrill
Copy link
Contributor Author

jcbrill commented May 20, 2025

Maybe the PS 5 assemblies are not optimized?

Powershell.exe is invoked when VSCMD_SKIP_SENDTELEMETRY is undefined. This could possibly incur the powershell setup cost, if any, before vcpkg initialization. It could appear in 2019 if that were the case although the magnitude could be very different.

@mwichmann
Copy link
Collaborator

The .net assembly thing is probably something different, but it does involve populating something called a "Native Image Cache". In a StackOverflow post, someone had reported seeing first-time startup of around a minute, which sounds oddly familiar.

I snipped this attempted explanation from "the net":

Ngen stands for Native Image Generator, and it’s a critical component of the .NET Framework. Its primary purpose is to create native images from managed assemblies. But what exactly does that mean?

When you run a .NET application, such as PowerShell, it typically involves compiling CIL (Common Intermediate Language) code into machine code during runtime. This just-in-time (JIT) compilation adds a small overhead to the application’s startup time. Ngen eliminates this overhead by pre-compiling the CIL code into native machine code. Essentially, it’s like translating a book from one language to another ahead of time, making it ready for immediate consumption.

PowerShell relies on various assemblies and libraries to function properly. These assemblies contain the code that makes PowerShell work, but they are initially in CIL format. Without Ngen, PowerShell would have to compile this code every time it starts, which can be time-consuming.

By using Ngen to pre-emptively create native images for the assemblies that PowerShell relies on, we can achieve a significant boost in performance. Here’s why it matters:

  1. Faster Startup Times: When you open PowerShell, you want it to be ready for action as quickly as possible. Ngen helps achieve this by reducing the time it takes to load and initialize essential assemblies.

It's probably different because the cache isn't in a powershell-specific place, so the PS path fiddling should not affect it. The paths are

C:\Windows\assembly\NativeImages_v4.0.30319_64 - for 64-bit images
C:\Windows\assembly\NativeImages_v4.0.30319_32 - for 32-bit images

…eCleanup, and PSModuleAnalysisCachePath into the limited msvc environment when defined.

Environment variables added to msvc environment import list:
* POWERSHELL_TELEMETRY_OPTOUT: opt-out of telemetry.
* PSDisableModuleAnalysisCacheCleanup: disable checking for modules that no longer exist when writing module analysis cache.
* PSModuleAnalysisCachePath: path and filename used to cache data about modules and their cmdlets.

The PSModuleAnalysisCachePath is defined in windows runners 2019, 2022, and 2025.  The significant delays experienced in the 2022 and 2025 runners when running the msvc batch files due to vcpkg with powershell 5 and without PSModulePath defined appear to be eliminated when the powershell module analysis cache path is propagated to the msvc environment.

Additional changes:
* Move the PS7 path after the PS5 path.
* Add the elapsed execution time of the msvc batch files to the debug log.
@jcbrill
Copy link
Contributor Author

jcbrill commented May 26, 2025

@mwichmann Windows runner 2022 and 2025 root cause of delays likely identified.

Experimentation keeps pointing to the powershell module analysis cache as likely having a significant impact.

The environment variable PSModuleAnalysisCachePath is defined in windows runners 2019, 2022, and 2025.

The significant delays experienced in the 2022 and 2025 runners when running the msvc batch files due to vcpkg with powershell 5 and without PSModulePath defined appear to be eliminated when the powershell module analysis cache path PSModuleAnalysisCachePath is propagated to the limited msvc environment.

The powershell module analysis cache appears to be pre-populated in the windows runners (which makes sense).

Running PS 5 with the module analysis cache location PSModuleAnalysisCachePath and without PSModulePath defined appears to eliminate the significant delay.

The impact of PSModuleAnalysisCachePath appears more significant than adding PS 7 to the path in the windows runners.

Additional environment variables propagated to the msvc environment when defined:

  • POWERSHELL_TELEMETRY_OPTOUT: opt-out of telemetry.
  • PSDisableModuleAnalysisCacheCleanup: disable checking for modules that no longer exist when writing module analysis cache.
  • PSModuleAnalysisCachePath: path and filename used to cache data about modules and their cmdlets.

The PS 7 executable path was moved after the PS 5 executable path.

The text in CHANGES.txt and RELEASE.txt still need to be updated.

@mwichmann
Copy link
Collaborator

Sounds good.

I haven't changed my opinion: keeping environment variables strictly contained in SCons is a recipe for problems when they come from a complex and carefully curated system (such as the Microsoft compiler suite - more than just C++).

By the way (nitpicking), I've recently learned that we probably shouldn't call it "PowerShell 7", it's PowerShell Core, the (supposedly) open-sourced version that rebased onto .NET core - that's able to run cross-platform. That actually started with v6, has now moved on to v7,

@jcbrill
Copy link
Contributor Author

jcbrill commented May 26, 2025

I haven't changed my opinion: keeping environment variables strictly contained in SCons is a recipe for problems when they come from a complex and carefully curated system (such as the Microsoft compiler suite - more than just C++).

I agree in principle. The limited msvc environment was carefully crafted 20 years ago. Not so much today.

Unfortunately, using the OS environment comes with its own challenges. I believe the msvc batch files preserve the OS definitions of INCLUDE, LIB, and LIBPATH (and possibly others) if they exist in the environment which would likely be undesirable when running the msvc batch files from SCons.

On the other hand, it may be easier to maintain an "exclude" list rather than an "include" list.

By the way (nitpicking), I've recently learned that we probably shouldn't call it "PowerShell 7", it's PowerShell Core, the (supposedly) open-sourced version that rebased onto .NET core - that's able to run cross-platform. That actually started with v6, has now moved on to v7,

Fair enough.

Referring to the powershell executable versions which are approximately 5.1(?) and 7.5(?) seemed easier than referring to powershell.exe vs pwsh.exe directly.

jcbrill referenced this pull request in LeonarddeR/scons-examples May 26, 2025
@mwichmann
Copy link
Collaborator

Unfortunately, using the OS environment comes with its own challenges.

Of course. 20+ years ago the developers didn't put in this scheme for no reason, I'm not saying that. The risk of bad settings in a developer's environment, that produce unreliable results, or non-reproducible ones ("nobody else has the same settings") are very real.

I just re-ran the Intel OneAPI compiler suite's setup on Linux, which, like msvc (and maybe worse), hunts around for the setup scripts of installed components, and in the end it added 42 new environment variables as well as prepending eight path segments to the front of PATH. (they aren't all unique to OneAPI, but happened not to be set in my environment, like MANPATH, LD_LIBRARY_PATH, PKG_CONFIG_PATH and PYTHONPATH). One reason I haven't gotten to reworking the Intel compiler tool setup is I have no enthusiasm for figuring out which ones of those have to be made available to the SCons execution environment - clearly a lot of them don't, but some do. And then figuring out how to generate them, since like the msvc ones, they have to be recomputed every time versions change. (On Windows there's a "Developer Command Prompt" like for msvc, and in fact, it integrates with msbuild or visual studio, so I think you can't just run the Intel bits in isolation).

@jcbrill
Copy link
Contributor Author

jcbrill commented May 26, 2025

Of course. 20+ years ago the developers didn't put in this scheme for no reason, I'm not saying that. The risk of bad settings in a developer's environment, that produce unreliable results, or non-reproducible ones ("nobody else has the same settings") are very real.

I was attempting to say that the limited msvc environment has not kept up with the changes introduced in successive Windows versions (e.g., 2000, 7, 8, 10, 11, etc).

The burr in my saddle is not passing along all of the Windows system default variables.

That would not have solved the powershell issue though.

One could argue that powershell (5.X) has been a Windows system program for a long time and the optional environment variables should be supported (especially those in common with Powershell Core).

There are two other "issues" with the current SCons processing of the msvc environment environment output:

  1. All stdout output is searched for environment variable assignments due to the set command following the msvc batch file invocation (i.e., ... vcvars64.bat & set). There really is no reason to process all of stdout looking for environment variable settings. This goes sideways when VSCMD_DEBUG is specified as the environment variables are dumped many times. An improved method is the echo a line that is known not to be produced in the output and then partition the stdout output based on the echoed line. This way the first partition contains the msvc output information and last partition contains only the set information. For example, ...vcvars64.bat & echo 0123ABCdefGEHijk9876 & set.
  2. All "kept" environment variables are assumed to be paths. A list of values is collected when processing the output. As mentioned above, when VSCMD_DEBUG is specified, the same variable may appear multiple times which would collect a list of values. Environment variables can be classified as supporting a list of paths (e.g., PATH), a path scalar (e.g., TEMP), or a scalar (e.g., VSCMD_SKIP_SENDTELEMETRY). Other categories are possible as well.

One reason I haven't gotten to reworking the Intel compiler tool setup is I have no enthusiasm for figuring out which ones of those have to be made available to the SCons execution environment - clearly a lot of them don't, but some do. And then figuring out how to generate them, since like the msvc ones, they have to be recomputed every time versions change. (On Windows there's a "Developer Command Prompt" like for msvc, and in fact, it integrates with msbuild or visual studio, so I think you can't just run the Intel bits in isolation).

SCons does not keep all of the environment variables that the msvc batch files create. I'm not sure keeping them is problematic. It might even make debugging users issues easier as the environment could be dumped.

The simplest way of keeping new variables is a difference of the set of environment variables coming out of the batch files and the set of those that went into the environment. One could keep all of the new variables.

The environment variables in both environments could be compared to see if they changed and if so, process the changed variables. This gets trickier depending on the meaning of the environment variable and whether or not a change should be preserved.

Deletions could be handled as well.

@jcbrill
Copy link
Contributor Author

jcbrill commented May 26, 2025

@mwichmann For your amusement...

This is the entirety of the MSVC 6.0 vcvars32.bat batch file (installed to a custom location):

@echo off
rem
rem Root of Visual Developer Studio Common files.
set VSCommonDir=C:\Software\MSVS-1998-60-Pro\Common

rem
rem Root of Visual Developer Studio installed files.
rem
set MSDevDir=C:\Software\MSVS-1998-60-Pro\Common\msdev98

rem
rem Root of Visual C++ installed files.
rem
set MSVCDir=C:\Software\MSVS-1998-60-Pro\VC98

rem
rem VcOsDir is used to help create either a Windows 95 or Windows NT specific path.
rem
set VcOsDir=WIN95
if "%OS%" == "Windows_NT" set VcOsDir=WINNT

rem
echo Setting environment for using Microsoft Visual C++ tools.
rem

if "%OS%" == "Windows_NT" set PATH=%MSDevDir%\BIN;%MSVCDir%\BIN;%VSCommonDir%\TOOLS\%VcOsDir%;%VSCommonDir%\TOOLS;%PATH%
if "%OS%" == "" set PATH="%MSDevDir%\BIN";"%MSVCDir%\BIN";"%VSCommonDir%\TOOLS\%VcOsDir%";"%VSCommonDir%\TOOLS";"%windir%\SYSTEM";"%PATH%"
set INCLUDE=%MSVCDir%\ATL\INCLUDE;%MSVCDir%\INCLUDE;%MSVCDir%\MFC\INCLUDE;%INCLUDE%
set LIB=%MSVCDir%\LIB;%MSVCDir%\MFC\LIB;%LIB%

set VcOsDir=
set VSCommonDir=

VC 6.0 was used in anger well past VS 2005.

@bdbaddog
Copy link
Contributor

@mwichmann Windows runner 2022 and 2025 root cause of delays likely identified.

Experimentation keeps pointing to the powershell module analysis cache as likely having a significant impact.

The environment variable PSModuleAnalysisCachePath is defined in windows runners 2019, 2022, and 2025.

The significant delays experienced in the 2022 and 2025 runners when running the msvc batch files due to vcpkg with powershell 5 and without PSModulePath defined appear to be eliminated when the powershell module analysis cache path PSModuleAnalysisCachePath is propagated to the limited msvc environment.

The powershell module analysis cache appears to be pre-populated in the windows runners (which makes sense).

Running PS 5 with the module analysis cache location PSModuleAnalysisCachePath and without PSModulePath defined appears to eliminate the significant delay.

The impact of PSModuleAnalysisCachePath appears more significant than adding PS 7 to the path in the windows runners.

Additional environment variables propagated to the msvc environment when defined:

* `POWERSHELL_TELEMETRY_OPTOUT`: opt-out of telemetry.

* `PSDisableModuleAnalysisCacheCleanup`: disable checking for modules that no longer exist when writing module analysis cache.

* `PSModuleAnalysisCachePath`: path and filename used to cache data about modules and their cmdlets.

The PS 7 executable path was moved after the PS 5 executable path.

The text in CHANGES.txt and RELEASE.txt still need to be updated.

Is this only in the case that vcpkg is installed?

@jcbrill
Copy link
Contributor Author

jcbrill commented May 27, 2025

I'm not sure what you are asking.

The delays were caused by the configuration of the limited msvc environment and the execution of a powershell script that likely involves loading additional commands/modules which in turn involves the powershell module analysis cache.

Installing vcpkg causes a powershell script to be run. The limited msvc environment causes the significant delays with SCons 4.9.1 and the windows 2022 and 2025 runners. vcpkg initialization appears to trigger powershell initialization delays with the module analysis cache.

The previous PR added pwsh.exe to the msvc environment path which appears to eliminate the significant delays. Including PSModuleAnalysisCachePath in the msvc environment appears to eliminate the significant delays when using powershell.exe.

The PSModuleAnalysisCachePath value in the windows runners is not the default location for the powershell module analysis cache.

Prior to the previous PR which added pwsh.exe to the path, any powershell.exe command or script invocation that required loading additional powershell commands/modules in the limited msvc environment without PSModuleAnalysisCachePath and PSModulePath defined would incurr a runtime penalty. With vcpkg installed, when vcpkg initializes this causes powershell to initialize as well. Subsequent invocations of powershell still pay a penalty but it is reduced.

Running this command powershell.exe -Command "& {$Env:PSModulePath | Out-String -Width 4096}" in the limited msvc environment demonstrates the behavior. Simply printing the PSModulePath incurs a delay when both PSModuleAnalysisCachePath and PSModulePath are undefined.

Without PSModuleAnalysisCachePath and PSModulePath, 3 consecutive invocations of the powershell command and 3 consecutive vcvars invocations result in (i.e., the SCons 4.9.1 configuration):

ELAPSED_TIME=53.09, cmd='powershell.exe -Command "& {$Env:PSModulePath | Out-String -Width 4096}"'
ELAPSED_TIME=20.26, cmd='powershell.exe -Command "& {$Env:PSModulePath | Out-String -Width 4096}"'
ELAPSED_TIME=20.28, cmd='powershell.exe -Command "& {$Env:PSModulePath | Out-String -Width 4096}"'

ELAPSED_TIME=25.00 vc_script='C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise\\VC\\Auxiliary\\Build\\vcvars64.bat'
ELAPSED_TIME=21.08 vc_script='C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise\\VC\\Auxiliary\\Build\\vcvars64.bat'
ELAPSED_TIME=23.90 vc_script='C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise\\VC\\Auxiliary\\Build\\vcvars64.bat'

The first time of 53.09 seconds typically ranges from 30-120 seconds. Subsequent invocations are typically around 20 seconds.

With PSModuleAnalysisCachePath and without PSModulePath, 3 consecutive invocations of the powershell command and 3 consecutive vcvars invocations result in (pwsh.exe not on path):

ELAPSED_TIME=0.38, cmd='powershell.exe -Command "& {$Env:PSModulePath | Out-String -Width 4096}"'
ELAPSED_TIME=0.31, cmd='powershell.exe -Command "& {$Env:PSModulePath | Out-String -Width 4096}"'
ELAPSED_TIME=0.25, cmd='powershell.exe -Command "& {$Env:PSModulePath | Out-String -Width 4096}"'
...
ELAPSED_TIME=2.52 vc_script='C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise\\VC\\Auxiliary\\Build\\vcvars64.bat'
ELAPSED_TIME=1.03 vc_script='C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise\\VC\\Auxiliary\\Build\\vcvars64.bat'
ELAPSED_TIME=1.02 vc_script='C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise\\VC\\Auxiliary\\Build\\vcvars64.bat'

This shows there is a cost without passing the existing PSModuleAnalysisCachePath when using powershell.exe.

There is an initial cost and subsequent cost for powershell.exe without the module analysis cache.
There is a minimal cost for pwsh.exe without the module analysis cache which is why adding the pwsh.exe location to the environment path "works" for vcpkg.

There is an initial cost to initialize vcpkg that appears to be larger than subsequent invocations.

vcpkg likely causes powershell.exe to load additional modules for the first time.

Note: powershell.exe is used to initialize sending telemetry data. In all windows runners, simply loading a DLL does not appear to cause a delay. This is why the runner 2019 runs were as expected.

This PR:

  • Adds PSModuleAnalysisCachePath to the msvc environment
  • Adds the known pwsh.exe path after the known powershell.exe path
  • Adds additional powershell specific variables to the msvc environment

@bdbaddog
Copy link
Contributor

I was curious if the slowdown only happens when vcpkg has been installed.

Also of note, a google finds that we could set a reasonable default for PSModuleAnalysisCachePath without copying the shell env.

PowerShell Env Variables

Above also gives (seems to at least) definitive answers about powershell's user of shell env variables.

@mwichmann
Copy link
Collaborator

I was curious if the slowdown only happens when vcpkg has been installed.

Also of note, a google finds that we could set a reasonable default for PSModuleAnalysisCachePath without copying the shell env.

I'd expect we want to be sure to get the one that's already been initialized, or we don't benefit from the cache warming effect. That may well turn out to be the default for that PS version - but if it was, then why do we see the problem with the variable unset (I have only PS 5 installed, and my cache is in AppData/Local/Microsoft/Windows/ModuleAnalysisCache - and the environment variable is never set). It's irritating that the two versions share a variable name, but not a default location. We know that vcpkg specifically will choose PS 7 (Core) if found. That doc says there's some sort of naming scheme for the actual cache file if you have several PS versions; I guess it's at that point you have to start setting the variable?

@jcbrill
Copy link
Contributor Author

jcbrill commented May 27, 2025

I was curious if the slowdown only happens when vcpkg has been installed.

For the msvc batch file invocations in the windows 2022 and 2025 runners: yes.

I don't know if there are any additional uninstalled extra VS components that rely on powershell that could have materialized similar behavior.

The powershell.exe invocation to setup telemetry in the windows 2019/2022/2035 runners does not manifest a significant slowdown.

Also of note, a google finds that we could set a reasonable default for PSModuleAnalysisCachePath without copying the shell env.

From earlier comments:

  • The environment variable PSModuleAnalysisCachePath is defined in windows runners 2019, 2022, and 2025.
  • The powershell module analysis cache appears to be pre-populated in the windows runners (which makes sense).
  • The PSModuleAnalysisCachePath value in the windows runners is not the default location for the powershell module analysis cache.

In the windows 2019, 2022, and 2025 windows runners, PSModuleAnalysisCachePath is defined and the cache is already populated in that location.

The reason for importing PSModuleAnalysisCachePath is that the cache is already populated. This appears to benefit both powershell.exe and pwsh.exe. The benefit for pwsh.exe is negligible though.

It may not make any sense to define our own location in the environment as powershell will use its own default location when PSModuleAnalysisCachePath is undefined.

The benefit for the windows runners is that the powershell analysis module cache appears to be populated before the msvc batch files are invoked.

Even if the cache were not pre-prepopulated, there may be a good reason why a user would like the cache to be populated in a certain location.

For example, in the windows runners, if the cache were moved to a different drive (e.g., D:) there might be a runtime benefit. Perhaps a user is manually saving and restoring the powershell module analysis cache from their own known location.

The powershell environment configuration could be

PowerShell Env Variables

Above also gives (seems to at least) definitive answers about powershell's user of shell env variables.

That was the source, and contains the rationale, for adding POWERSHELL_TELEMETRY_OPTOUT, PSDisableModuleAnalysisCacheCleanup, and PSModuleAnalysisCachePath to the import list if defined in the user's shell environment.

Some amount of caution needs to be exercised in assuming all environments will behave like the windows 2019/2022/2025 runners.

For the limited msvc environment, importing PSModuleAnalysisCachePath and adding the pwsh.exe path seems prudent given the variety of other system configurations in the wild.

At present, PSModulePath is not imported into the limited msvc environment. This does not seem to matter for the windows runners. It might for other system configurations.

Future work might consider what was described in the alternate branch mentioned above:

  • Add pwsh.exe to the limited msvc environment path if pwsh.exe exists on the shell environment path and in the same relative order for powershell.exe and pwsh.exe as the shell environment path.
  • Add a subset of known PSModulePath locations for the current User, AllUsers, and Installation locations for powershell.exe and pwsh.exe to the msvc limited environment in the same relative order as the shell environment PSModulePath.
  • Import PSModuleAnalysisCachePath if not already adopted in this PR.

P.S.: A similar argument could be made for importing ProgramData into the limited msvc environment. The vswhere query to detect msvc installations uses the OS environment with has a definition of ProgramData. Attempting to run vswhere from within the limited msvc environment, which does not import the ProgramData variable and value, returns an empty list because the data is stored in the ProgramData location. That is why the program banners display a general version number of the SCons msvc output rather than the specific version number as in the OS environment and why some projects can be seen adding ProgramData to their SCons environment.

@jcbrill
Copy link
Contributor Author

jcbrill commented May 27, 2025

@mwichmann I was in the process of writing the above and committing before seeing your post. Please check the following for accuracy.

I'd expect we want to be sure to get the one that's already been initialized, or we don't benefit from the cache warming effect.

This.

That may well turn out to be the default for that PS version - but if it was, then why do we see the problem with the variable unset (I have only PS 5 installed, and my cache is in AppData/Local/Microsoft/Windows/ModuleAnalysisCache - and the environment variable is never set).

There is a cache persistence difference between running on a physical pc or a virtual machine environment compared to the windows runners. On a physical pc or a virtual machine, the default cache once constructed (if at all) is persistent. To emulate the windows runner behavior in a virtual machine, one would have to "reset the vm state" after each run.

The default locations of the module analysis cache when PSModuleAnalysisCachePath are:

  • Windows PowerShell 5.1: $Env:LOCALAPPDATA\Microsoft\Windows\PowerShell
  • PowerShell 6.0 and higher: $Env:LOCALAPPDATA\Microsoft\PowerShell
  • Non-Windows default: ~/.cache/powershell

For the following, the size of the pre-populated cache in the Windows 2022 runner is 1,610,258 bytes (~1.6MB) as reported by python's os.path.getsize(file). It is likely that the cache is constructed with 7.x and 5.x modules based on observed behavior. I suspect that the cache is larger due to the inclusion of the Azure modules.

  • For SCons 4.9.1 and earlier:

    pwsh.exe (7.x) was not on the msvc environment path and PSModuleAnalysisCachePath and PSModulePath are not included in the msvc environment.

    This causes powershell.exe (5.x) to use a default module analysis cache path location and to use a default PSModulePath.

    The default PSModulePath construction does not include the PS 7 (Core) known module locations. Using the 5.x modules and processing the cache is slow.

  • For the previous PR:

    pwsh.exe is on the msvc environment path and PSModuleAnalysisCachePath and PSModulePath are not included in the msvc environment.

    This causes pwsh.exe (7.x) to use a default module analysis cache path location and to use a default PSModulePath.

    The default PSModulePath construction includes the 7.x known module locations and some of the 5.x known locations as well.

    Some of the the powershell 7.x modules shadow the 5.x modules. The 7.x modules appear faster than their 5.x counterparts.

  • For this discussion thread:

    pwsh.exe (7.x) not on the msvc environment path, PSModuleAnalysisCachePath included, and PSModulePath not included in the msvc environment.

    This caused powershell.exe (5.x) to use the defined cache path location and to use a default PSModulePath. The cache is already populated.

    My guess is that cache was built with the 7.x modules shadowing the 5.x modules. This would imply that the 7.x modules are retrieved from the cache and employed.

    Given that the module cache is already populated, I believe this is equivalent to using powershell.exe with a PSModulePath with the 7.x module locations preceding the 5.x module locations which is known to eliminate the slowdown as well.

  • This PR:

    pwsh.exe (7.x) is on the msvc environment path. PSModuleAnalysisCachePath is included in the msvc environment. PSModulePath is not included in the msvc environment.

    For the windows runners, pwsh.exe will be used and the module cache will be used. Either in isolation solves the slowdown issue. In combination, one would suspect it to be faster than either in isolation. Using the pre-populated cache likely includes all elements of the OS PSModulePath which is not passed to the msvc environment.

    In the windows runners, using powershell.exe will also not experience the slowdown.

The only way to guarantee the same behavior as the shell environment is to: 1) add pwsh.exe to the msvc environment path, 2) include PSModuleAnalysisPath in the msvc environment, and 3) include PSModulePath in the msvc environment.

The prior PR does 1. This PR does 1 and 2. A limited form of 3 was discussed in my previous post under "future work".

It's irritating that the two versions share a variable name, but not a default location.

It is confusing.

We know that vcpkg specifically will choose PS 7 (Core) if found. That doc says there's some sort of naming scheme for the actual cache file if you have several PS versions; I guess it's at that point you have to start setting the variable?

That would be my guess.

PS 7 (Core) does not appear to add the hex suffix to the end of the PSModuleAnalysisCachePath.

@jcbrill
Copy link
Contributor Author

jcbrill commented May 27, 2025

@mwichmann Research implementation windows runner logs showing the complete OS environment and limited msvc environment, and the powershell module analysis cache file sizes (0 if non-existent). Let me know if you have any questions.

These logs are representative of the windows runner findings to date. The logs also provide a decent view of the windows runner configuration.

The table below contains modified versions of the windows 2022 logs for four experiments with the pure python implementation. The windows runner timestamp prefix was removed for readability.

This is using the research implementation and not the SCons implementation.

Log records legend:

  • SConstruct:log_environ#1395: os.environ prefix
    Windows runner shell environment
  • SConstruct:_test_scons#1581: env prefix
    msvc limited environment
  • ModuleAnalysisCache:
    Known powershell locations and cache file sizes
PS No Cache Cache
5.x Exp_PS5_NoCache_msvc-python.txt Exp_PS5_Cache_msvc-python.txt
7.x Exp_PS7_NoCache_msvc-python.txt Exp_PS7_Cache_msvc-python.txt

PS 5.x and No Cache: SCons 4.9.1 and earlier.

PS 7.x and No Cache: Previous PR (i.e., current SCons master).

PS 7.x and Cache: This PR.

PS 5.x and Cache: This PR (implied if pwsh.exe not on path).

@jcbrill
Copy link
Contributor Author

jcbrill commented May 28, 2025

I think that PSModulePath should be added to the imported shell variable list as well.

Adding a shell environment variable to the limited msvc environment does not mean it will be preserved in the caller's SCons environment. Other than PATH, none of the shell environment variables imported into the limited msvc environment are exported to the caller's SCons environment.

Effectively, powershell running in the limited msvc environment would be roughly equivalent to running powershell in the shell environment subject to the minimal PATH and PATHEXT variable values and without a handful of powershell specific variables listed in the MS documentation.

This same argument applies for adding known Windows system variables (e.g., ProgramData, PROCESSOR_ARCHITECTURE, etc.) as well.

The diagram below shows the shell environment members of the limited environment in which the msvc batch files are invoked and the environment variables that are preserved in the caller's SCons environment.

flowchart TD;
    
    A[
    **Construct Limited MSVC Environment**<br>
    *==SCons/Platform/win32.py==*<br/>
    PATH &lsqb;minimal&rsqb;<sup>1</sup>
    PATHEXT &lsqb;minimal&rsqb;<sup>2</sup>
    SystemDrive
    SystemRoot
    TEMP
    TMP
    USERPROFILE<br/>
    *==SCons/Tool/MSCommon/Common.py==*<br/>
    PATH &lsqb;minimal extended&rsqb;<sup>3</sup>
    COMSPEC
    OS
    VS170COMNTOOLS
    VS160COMNTOOLS
    VS150COMNTOOLS
    VS140COMNTOOLS
    VS120COMNTOOLS
    VS110COMNTOOLS
    VS100COMNTOOLS
    VS90COMNTOOLS
    VS80COMNTOOLS
    VS71COMNTOOLS
    VSCOMNTOOLS
    MSDevDir
    VSCMD_DEBUG
    VSCMD_SKIP_SENDTELEMETRY
    windir
    VCPKG_DISABLE_METRICS
    VCPKG_ROOT<br>
    *==Proposed SCons PR 4720==*<br/>
    POWERSHELL_TELEMETRY_OPTOUT
    PSDisableModuleAnalysisCacheCleanup
    PSModuleAnalysisCachePath
    PSModulePath &lsqb;subset&rsqb;<sup>4</sup>
    ]
    
    B[
    **Call MSVC Batch File**<br/>
    vcvars64.bat & set
    ]
    
    C[
    **Extend Caller SCons env&lsqb;&quot;ENV&quot;&rsqb;**<br>
    *==SCons/Tool/MSCommon/Common.py==*<br/>
    INCLUDE
    LIB
    LIBPATH
    PATH
    VSCMD_ARG_app_plat
    VCINSTALLDIR
    VCToolsInstallDir
    ]
    
    A-->B;
    B-->C;
Loading

Diagram footnotes:

  1. PATH
    C:\Windows\System32
  2. PATHEXT
    .COM;.EXE;.BAT;.CMD
  3. PATH [powershell paths in shell environment order]
    C:\Windows\System32
    C:\Windows\System32\Wbem
    C:\Windows\System32\WindowsPowerShell\v1.0
    C:\Program Files\PowerShell\7
  4. PSModulePath [paths in shell environment order]
    C:\Program Files\PowerShell\Modules
    C:\Program Files\PowerShell\7\Modules
    C:\Program Files\WindowsPowerShell\Modules
    C:\Windows\System32\WindowsPowerShell\v1.0\Modules

Notes:

  • Importing and exporting the same variable into and out of the msvc batch environment may not be straightforward (e.g., the msvc batch files change an imported value).

Edit [2025-05-29] to reflect PR code changes:

  • Add PSModulePath to diagram.
  • Update footnote 3.
  • Add footnote 4.

jcbrill added 3 commits May 29, 2025 12:12
…PATH and add limited known shell PSModulePath paths.

Changes:
* Add the pwsh and powershell paths to the msvc environment PATH in the order discovered on the shell environment PATH.
* Add the pwsh and powershell AllUsers and installation PSModule paths to the msvc environment PSModulePath in the order discovered on the shell environment PSModulePath.
# Manually resolved conflicts:
#	RELEASE.txt
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants