From f47a44ed1e1671ce8db980c2ce26e973e91f2405 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Sun, 2 Aug 2026 15:30:23 +0700 Subject: [PATCH 1/6] perf: speed up Windows CI --- .github/actions/setup-windows/action.yml | 38 +++++++----- .github/scripts/windows/build_task.bat | 19 +++++- .../build_with_test_database_setup.ps1 | 33 ++++++++++ .../scripts/windows/setup_test_databases.ps1 | 57 +++++++++++++++++ .github/workflows/test-suite.yml | 30 +++++++-- ext/com_dotnet/Makefile.frag.w32 | 8 +++ ext/json/Makefile.frag.w32 | 10 +++ ext/standard/Makefile.frag.w32 | 2 +- win32/build/Makefile | 56 ++++++++++++++++- win32/build/config.w32 | 15 +++-- win32/build/confutils.js | 62 ++++++++++++++++--- 11 files changed, 295 insertions(+), 35 deletions(-) create mode 100644 .github/scripts/windows/build_with_test_database_setup.ps1 create mode 100644 .github/scripts/windows/setup_test_databases.ps1 diff --git a/.github/actions/setup-windows/action.yml b/.github/actions/setup-windows/action.yml index a5fa4cdddcb1..47f178578c3f 100644 --- a/.github/actions/setup-windows/action.yml +++ b/.github/actions/setup-windows/action.yml @@ -1,21 +1,31 @@ -name: Setup +name: Setup Windows build tools runs: using: composite steps: - - name: Setup MySQL - shell: cmd - run: | - mysqld --initialize-insecure - mysqld --install - net start MySQL - mysql --port=3306 --user=root --password="" -e "ALTER USER 'root'@'localhost' IDENTIFIED BY 'Password12!'; FLUSH PRIVILEGES;" - - name: Setup MSSQL + - name: Install jom shell: pwsh run: | - choco install sql-server-express -y --no-progress --install-arguments="/SECURITYMODE=SQL /SAPWD=Password12!" - - name: Setup PostgreSQL + $jomDirectory = Join-Path $env:RUNNER_TEMP "jom" + $jomArchive = Join-Path $env:RUNNER_TEMP "jom.zip" + Invoke-WebRequest https://download.qt.io/official_releases/jom/jom_1_1_7.zip -OutFile $jomArchive + if ((Get-FileHash $jomArchive -Algorithm SHA256).Hash -ne "4C8AF345586A9A08FBFD2F613FCAC748226D91A75627AA3581B297DD513046FE") { + throw "Unexpected jom archive checksum" + } + Expand-Archive $jomArchive -DestinationPath $jomDirectory + $jomDirectory | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + - name: Install sccache + if: ${{ env.CLANG_TOOLSET != '1' }} shell: pwsh run: | - $postgresService = if ($env:PHP_BUILD_CRT -eq "vs18") { "postgresql-x64-17" } else { "postgresql-x64-14" } - Set-Service -Name $postgresService -StartupType manual -Status Running - pwsh -Command { $env:PGPASSWORD="root"; & "$env:PGBIN\psql" -U postgres -c "ALTER USER postgres WITH PASSWORD 'Password12!';" } + $sccacheDirectory = Join-Path $env:RUNNER_TEMP "sccache-bin" + $sccacheCacheDirectory = Join-Path $env:RUNNER_TEMP "sccache" + $sccacheArchive = Join-Path $env:RUNNER_TEMP "sccache.zip" + Invoke-WebRequest https://github.com/mozilla/sccache/releases/download/v0.15.0/sccache-v0.15.0-x86_64-pc-windows-msvc.zip -OutFile $sccacheArchive + if ((Get-FileHash $sccacheArchive -Algorithm SHA256).Hash -ne "DCF489090AA5EF4C7D145E8B29F759124C803D636C3107F397BD50D425B5F341") { + throw "Unexpected sccache archive checksum" + } + Expand-Archive $sccacheArchive -DestinationPath $sccacheDirectory + (Join-Path $sccacheDirectory "sccache-v0.15.0-x86_64-pc-windows-msvc") | + Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + "SCCACHE_DIR=$sccacheCacheDirectory" | + Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append diff --git a/.github/scripts/windows/build_task.bat b/.github/scripts/windows/build_task.bat index 1177cef3be4d..976fa35dd86e 100644 --- a/.github/scripts/windows/build_task.bat +++ b/.github/scripts/windows/build_task.bat @@ -41,6 +41,7 @@ if "%CLANG_TOOLSET%" equ "1" ( cmd /c configure.bat ^ --enable-snapshot-build ^ + --enable-parallel-build ^ --disable-debug-pack ^ --without-analyzer ^ --enable-object-out-dir=%PHP_BUILD_OBJ_DIR% ^ @@ -49,9 +50,23 @@ cmd /c configure.bat ^ --disable-test-ini if %errorlevel% neq 0 exit /b 3 -nmake /NOLOGO +if "%CLANG_TOOLSET%" equ "1" goto build_clang + +sccache --zero-stats +jom /NOLOGO CC="sccache cl.exe" +if %errorlevel% neq 0 exit /b 3 +jom /NOLOGO CC="sccache cl.exe" comtest.dll if %errorlevel% neq 0 exit /b 3 -nmake /NOLOGO comtest.dll +sccache --show-stats +sccache --stop-server +goto build_complete + +:build_clang +jom /NOLOGO if %errorlevel% neq 0 exit /b 3 +jom /NOLOGO comtest.dll +if %errorlevel% neq 0 exit /b 3 + +:build_complete exit /b 0 diff --git a/.github/scripts/windows/build_with_test_database_setup.ps1 b/.github/scripts/windows/build_with_test_database_setup.ps1 new file mode 100644 index 000000000000..ca0323e0ce9a --- /dev/null +++ b/.github/scripts/windows/build_with_test_database_setup.ps1 @@ -0,0 +1,33 @@ +$ErrorActionPreference = "Stop" + +$databaseSetup = Join-Path $PSScriptRoot "setup_test_databases.ps1" +$databaseJob = Start-Job -FilePath $databaseSetup + +try { + $buildStopwatch = [System.Diagnostics.Stopwatch]::StartNew() + & (Join-Path $PSScriptRoot "build.bat") + $buildExitCode = $LASTEXITCODE + $buildStopwatch.Stop() + Write-Host "PHP build duration: $($buildStopwatch.Elapsed)" + + $databaseWaitStopwatch = [System.Diagnostics.Stopwatch]::StartNew() + Wait-Job -Job $databaseJob | Out-Null + $databaseWaitStopwatch.Stop() + Write-Host "Database wait after PHP build: $($databaseWaitStopwatch.Elapsed)" + + $databaseState = $databaseJob.State + $databaseError = $databaseJob.ChildJobs[0].JobStateInfo.Reason + Receive-Job -Job $databaseJob -ErrorAction Continue + + if ($buildExitCode -ne 0) { + throw "PHP build exited with code $buildExitCode" + } + if ($databaseState -ne "Completed") { + throw "Test database setup failed: $databaseError" + } +} finally { + if ($databaseJob.State -eq "Running") { + Stop-Job -Job $databaseJob + } + Remove-Job -Job $databaseJob +} diff --git a/.github/scripts/windows/setup_test_databases.ps1 b/.github/scripts/windows/setup_test_databases.ps1 new file mode 100644 index 000000000000..98de1cbadded --- /dev/null +++ b/.github/scripts/windows/setup_test_databases.ps1 @@ -0,0 +1,57 @@ +$ErrorActionPreference = "Stop" + +function Invoke-NativeCommand { + param( + [string] $Description, + [string] $FilePath, + [string[]] $ArgumentList + ) + + & $FilePath @ArgumentList + if ($LASTEXITCODE -ne 0) { + throw "$Description exited with code $LASTEXITCODE" + } +} + +$totalStopwatch = [System.Diagnostics.Stopwatch]::StartNew() +$sqlServerStopwatch = [System.Diagnostics.Stopwatch]::StartNew() + +Invoke-NativeCommand "SQL Server Express installation" "choco.exe" @( + "install", + "sql-server-express", + "--version=2022.16.0.20260305", + "-y", + "--no-progress", + "--install-arguments=/SECURITYMODE=SQL /SAPWD=Password12!" +) +$sqlServerStopwatch.Stop() +Write-Host "SQL Server Express setup duration: $($sqlServerStopwatch.Elapsed)" + +$mysqlStopwatch = [System.Diagnostics.Stopwatch]::StartNew() +Invoke-NativeCommand "MySQL initialization" "mysqld.exe" @("--initialize-insecure") +Invoke-NativeCommand "MySQL service installation" "mysqld.exe" @("--install") +Invoke-NativeCommand "MySQL service startup" "net.exe" @("start", "MySQL") +Invoke-NativeCommand "MySQL root account setup" "mysql.exe" @( + "--port=3306", + "--user=root", + "-e", + "ALTER USER 'root'@'localhost' IDENTIFIED BY 'Password12!'; FLUSH PRIVILEGES;" +) +$mysqlStopwatch.Stop() +Write-Host "MySQL setup duration: $($mysqlStopwatch.Elapsed)" + +$postgresStopwatch = [System.Diagnostics.Stopwatch]::StartNew() +$postgresService = if ($env:PHP_BUILD_CRT -eq "vs18") { "postgresql-x64-17" } else { "postgresql-x64-14" } +Set-Service -Name $postgresService -StartupType Manual -Status Running +$env:PGPASSWORD = "root" +Invoke-NativeCommand "PostgreSQL account setup" "$env:PGBIN\psql.exe" @( + "-U", + "postgres", + "-c", + "ALTER USER postgres WITH PASSWORD 'Password12!';" +) +$postgresStopwatch.Stop() +Write-Host "PostgreSQL setup duration: $($postgresStopwatch.Elapsed)" + +$totalStopwatch.Stop() +Write-Host "Total database setup duration: $($totalStopwatch.Elapsed)" diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index e269ed336408..e7bd767005da 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -857,10 +857,14 @@ jobs: PLATFORM: ${{ matrix.x64 && 'x64' || 'x86' }} THREAD_SAFE: "${{ matrix.zts && '1' || '0' }}" INTRINSICS: "${{ matrix.zts && 'AVX2' || '' }}" - PARALLEL: -j2 + PARALLEL: -j4 OPCACHE: "${{ matrix.opcache && '1' || '0' }}" ASAN: "${{ matrix.asan && '1' || '0' }}" CLANG_TOOLSET: "${{ matrix.clang && '1' || '0' }}" + SCCACHE_CACHE_SIZE: 1G + SCCACHE_IGNORE_SERVER_IO_ERROR: "1" + SCCACHE_CACHE_KEY: windows-sccache-v2-php${{ join(fromJson(inputs.branch).version, '.') }}-${{ fromJson(inputs.branch).jobs.WINDOWS.config.vs_crt_version }}-${{ matrix.x64 && 'x64' || 'x86' }}-zts${{ matrix.zts && '1' || '0' }}-asan${{ matrix.asan && '1' || '0' }}-${{ github.sha }} + SCCACHE_CACHE_RESTORE_PREFIX: windows-sccache-v2-php${{ join(fromJson(inputs.branch).version, '.') }}-${{ fromJson(inputs.branch).jobs.WINDOWS.config.vs_crt_version }}-${{ matrix.x64 && 'x64' || 'x86' }}-zts${{ matrix.zts && '1' || '0' }}-asan${{ matrix.asan && '1' || '0' }}- steps: - name: git config run: git config --global core.autocrlf false && git config --global core.eol lf @@ -868,10 +872,28 @@ jobs: uses: actions/checkout@v6 with: ref: ${{ fromJson(inputs.branch).ref }} - - name: Setup + - name: Setup Windows build tools uses: ./.github/actions/setup-windows - - name: Build - run: .github/scripts/windows/build.bat + - name: Restore MSVC compiler cache + if: ${{ !matrix.clang }} + continue-on-error: true + uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 + with: + path: ${{ runner.temp }}\sccache + key: ${{ env.SCCACHE_CACHE_KEY }} + restore-keys: ${{ env.SCCACHE_CACHE_RESTORE_PREFIX }} + - name: Build PHP while preparing test databases + shell: pwsh + run: .github/scripts/windows/build_with_test_database_setup.ps1 + # Pull request caches are isolated to the PR merge ref and cannot update + # caches used by the base branch or other pull requests. + - name: Save MSVC compiler cache + if: ${{ !matrix.clang }} + continue-on-error: true + uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 + with: + path: ${{ runner.temp }}\sccache + key: ${{ env.SCCACHE_CACHE_KEY }} - name: Test run: .github/scripts/windows/test.bat FREEBSD: diff --git a/ext/com_dotnet/Makefile.frag.w32 b/ext/com_dotnet/Makefile.frag.w32 index ec16dfc2974e..34144c5bf0c8 100644 --- a/ext/com_dotnet/Makefile.frag.w32 +++ b/ext/com_dotnet/Makefile.frag.w32 @@ -2,9 +2,17 @@ $(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest_i.c: ext\com_dotnet\tests\comt -md $(BUILD_DIR)\ext\com_dotnet\tests\comtest midl /nologo /h $(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest.h /iid $(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest_i.c /tlb $(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest.tlb ext\com_dotnet\tests\comtest\comtest.idl +!if "$(PARALLEL_BUILD)" == "yes" +$(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest.obj: ext\com_dotnet\tests\comtest\comtest.cpp $(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest_i.c + $(PHP_CL) /nologo /c /Fo$(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest.obj /I $(BUILD_DIR)\ext\com_dotnet\tests\comtest ext\com_dotnet\tests\comtest\comtest.cpp + +$(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest_i.obj: $(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest_i.c + $(PHP_CL) /nologo /c /Fo$(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest_i.obj $(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest_i.c +!else $(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest.obj $(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest_i.obj: ext\com_dotnet\tests\comtest\comtest.cpp $(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest_i.c $(PHP_CL) /nologo /c /Fo$(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest.obj /I $(BUILD_DIR)\ext\com_dotnet\tests\comtest ext\com_dotnet\tests\comtest\comtest.cpp $(PHP_CL) /nologo /c /Fo$(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest_i.obj $(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest_i.c +!endif $(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest.dll: $(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest.obj $(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest_i.obj ext\com_dotnet\tests\comtest\comtest.def "$(LINK)" /nologo /dll /out:$(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest.dll $(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest.obj $(BUILD_DIR)\ext\com_dotnet\tests\comtest\comtest_i.obj /def:ext\com_dotnet\tests\comtest\comtest.def OleAut32.lib diff --git a/ext/json/Makefile.frag.w32 b/ext/json/Makefile.frag.w32 index 28cffc62e3ef..b3d16f2fb4cc 100644 --- a/ext/json/Makefile.frag.w32 +++ b/ext/json/Makefile.frag.w32 @@ -1,5 +1,15 @@ +!if "$(PARALLEL_BUILD)" == "yes" +ext\json\php_json_scanner_defs.h: ext\json\json_scanner.c +ext\json\json_scanner.c: ext\json\json_scanner.re ext\json\json_parser.tab.h + $(RE2C) $(RE2C_FLAGS) -t ext/json/php_json_scanner_defs.h -bci -o ext/json/json_scanner.c ext/json/json_scanner.re + +ext\json\json_parser.tab.h: ext\json\json_parser.tab.c +ext\json\json_parser.tab.c: ext\json\json_parser.y + $(BISON) $(BISON_FLAGS) --defines -l ext/json/json_parser.y -o ext/json/json_parser.tab.c +!else ext\json\json_scanner.c ext\json\php_json_scanner_defs.h: ext\json\json_scanner.re ext\json\json_parser.tab.h $(RE2C) $(RE2C_FLAGS) -t ext/json/php_json_scanner_defs.h -bci -o ext/json/json_scanner.c ext/json/json_scanner.re ext\json\json_parser.tab.c ext\json\json_parser.tab.h: ext\json\json_parser.y $(BISON) $(BISON_FLAGS) --defines -l ext/json/json_parser.y -o ext/json/json_parser.tab.c +!endif diff --git a/ext/standard/Makefile.frag.w32 b/ext/standard/Makefile.frag.w32 index d1003fb2b23c..79bbfb43a446 100644 --- a/ext/standard/Makefile.frag.w32 +++ b/ext/standard/Makefile.frag.w32 @@ -6,7 +6,7 @@ ext\standard\url_scanner_ex.c: ext\standard\url_scanner_ex.re cd $(PHP_SRC_DIR) $(RE2C) $(RE2C_FLAGS) -b -o ext/standard/url_scanner_ex.c ext/standard/url_scanner_ex.re -$(BUILD_DIR)\ext\standard\basic_functions.obj: $(PHP_SRC_DIR)\Zend\zend_language_parser.h +$(BUILD_DIR)\ext\standard\basic_functions.obj: Zend\zend_language_parser.h $(PHP_SRC_DIR)\ext\standard\tests\helpers\bad_cmd.exe: $(PHP_SRC_DIR)\ext\standard\tests\helpers\bad_cmd.c cd $(PHP_SRC_DIR)\ext\standard\tests\helpers diff --git a/win32/build/Makefile b/win32/build/Makefile index e9308bc43cdb..4d07daaaba8b 100644 --- a/win32/build/Makefile +++ b/win32/build/Makefile @@ -54,8 +54,31 @@ DEBUGGER_ARGS= all: generated_files $(EXT_TARGETS) $(PECL_TARGETS) $(SAPI_TARGETS) test_helpers +!if "$(PARALLEL_BUILD)" == "yes" +BUILD_DIR_TARGET=build_dirs +!else build_dirs: $(BUILD_DIR) $(BUILD_DIRS_SUB) $(BUILD_DIR_DEV) +BUILD_DIR_TARGET=$(BUILD_DIR) $(BUILD_DIRS_SUB) $(BUILD_DIR_DEV) +!endif +!if "$(PARALLEL_BUILD)" == "yes" +!if $(RE2C) == "" +generated_files: build_dirs .SYNC \ + Zend\zend_ini_parser.c Zend\zend_ini_parser.h \ + Zend\zend_language_parser.c Zend\zend_language_parser.h \ + sapi\phpdbg\phpdbg_parser.c sapi\phpdbg\phpdbg_parser.h \ + $(PHPDEF) $(MCFILE) +!else +generated_files: build_dirs .SYNC \ + Zend\zend_ini_parser.c Zend\zend_ini_parser.h \ + Zend\zend_language_parser.c Zend\zend_language_parser.h \ + Zend\zend_ini_scanner.c Zend\zend_ini_scanner_defs.h \ + Zend\zend_language_scanner.c Zend\zend_language_scanner_defs.h \ + sapi\phpdbg\phpdbg_parser.c sapi\phpdbg\phpdbg_parser.h \ + sapi\phpdbg\phpdbg_lexer.c \ + $(PHPDEF) $(MCFILE) +!endif +!else !if $(RE2C) == "" generated_files: build_dirs \ Zend\zend_ini_parser.c Zend\zend_ini_parser.h \ @@ -71,10 +94,28 @@ generated_files: build_dirs \ sapi\phpdbg\phpdbg_parser.c sapi\phpdbg\phpdbg_lexer.c \ $(PHPDEF) $(MCFILE) !endif +!endif $(BUILD_DIR)\$(PHPDLL).def: $(PHP_DLL_DEF_SOURCES) type $(PHP_DLL_DEF_SOURCES) > $(BUILD_DIR)\$(PHPDLL).def +!if "$(PARALLEL_BUILD)" == "yes" +# Bison and re2c generate two files per invocation. Give the recipe to one +# target and make the side-effect output depend on it, so jom runs it once. +Zend\zend_ini_parser.h: Zend\zend_ini_parser.c +Zend\zend_ini_parser.c: Zend\zend_ini_parser.y + $(BISON) $(BISON_FLAGS) --output=Zend/zend_ini_parser.c -v -d Zend/zend_ini_parser.y + +Zend\zend_language_parser.h: Zend\zend_language_parser.c +Zend\zend_language_parser.c: Zend\zend_language_parser.y + $(BISON) $(BISON_FLAGS) --output=Zend/zend_language_parser.c -v -d Zend/zend_language_parser.y + @if "$(SED)" neq "" $(SED) -i "s,^int zendparse\(.*\),ZEND_API int zendparse\1,g" Zend/zend_language_parser.c + @if "$(SED)" neq "" $(SED) -i "s,^int zendparse\(.*\),ZEND_API int zendparse\1,g" Zend/zend_language_parser.h + +sapi\phpdbg\phpdbg_parser.h: sapi\phpdbg\phpdbg_parser.c +sapi\phpdbg\phpdbg_parser.c: sapi\phpdbg\phpdbg_parser.y + $(BISON) $(BISON_FLAGS) --output=sapi/phpdbg/phpdbg_parser.c -v -d sapi/phpdbg/phpdbg_parser.y +!else Zend\zend_ini_parser.c Zend\zend_ini_parser.h: Zend\zend_ini_parser.y $(BISON) $(BISON_FLAGS) --output=Zend/zend_ini_parser.c -v -d Zend/zend_ini_parser.y @@ -85,13 +126,24 @@ Zend\zend_language_parser.c Zend\zend_language_parser.h: Zend\zend_language_pars sapi\phpdbg\phpdbg_parser.c sapi\phpdbg\phpdbg_parser.h: sapi\phpdbg\phpdbg_parser.y $(BISON) $(BISON_FLAGS) --output=sapi/phpdbg/phpdbg_parser.c -v -d sapi/phpdbg/phpdbg_parser.y +!endif !if $(RE2C) != "" +!if "$(PARALLEL_BUILD)" == "yes" +Zend\zend_ini_scanner_defs.h: Zend\zend_ini_scanner.c +Zend\zend_ini_scanner.c: Zend\zend_ini_scanner.l + $(RE2C) $(RE2C_FLAGS) --case-inverted -cbdFt Zend/zend_ini_scanner_defs.h -oZend/zend_ini_scanner.c Zend/zend_ini_scanner.l + +Zend\zend_language_scanner_defs.h: Zend\zend_language_scanner.c +Zend\zend_language_scanner.c: Zend\zend_language_scanner.l + $(RE2C) $(RE2C_FLAGS) --case-inverted -cbdFt Zend/zend_language_scanner_defs.h -oZend/zend_language_scanner.c Zend/zend_language_scanner.l +!else Zend\zend_ini_scanner.c Zend\zend_ini_scanner_defs.h: Zend\zend_ini_scanner.l $(RE2C) $(RE2C_FLAGS) --case-inverted -cbdFt Zend/zend_ini_scanner_defs.h -oZend/zend_ini_scanner.c Zend/zend_ini_scanner.l Zend\zend_language_scanner.c Zend\zend_language_scanner_defs.h: Zend\zend_language_scanner.l $(RE2C) $(RE2C_FLAGS) --case-inverted -cbdFt Zend/zend_language_scanner_defs.h -oZend/zend_language_scanner.c Zend/zend_language_scanner.l +!endif sapi\phpdbg\phpdbg_lexer.c: sapi\phpdbg\phpdbg_lexer.l $(RE2C) $(RE2C_FLAGS) -cbdFo sapi/phpdbg/phpdbg_lexer.c sapi/phpdbg/phpdbg_lexer.l @@ -124,7 +176,7 @@ _VC_MANIFEST_EMBED_EXE= if exist $@.manifest $(MT) -nologo -manifest $@.manifest _VC_MANIFEST_EMBED_DLL= if exist $@.manifest $(MT) -nologo -manifest $@.manifest -outputresource:$@;2 !endif -$(PHPDLL_RES): win32\build\template.rc +$(PHPDLL_RES): generated_files $(MCFILE) win32\build\template.rc $(RC) /nologo /fo $(PHPDLL_RES) /d FILE_DESCRIPTION="\"PHP Script Interpreter\"" \ /d FILE_NAME="\"$(PHPDLL)\"" /d PRODUCT_NAME="\"PHP Script Interpreter\"" \ /I$(BUILD_DIR) /d MC_INCLUDE="\"$(MCFILE)\"" \ @@ -138,7 +190,7 @@ $(BUILD_DIR)\$(PHPDLL): generated_files $(PHPDEF) $(PHP_GLOBAL_OBJS) $(STATIC_EX $(BUILD_DIR)\$(PHPLIB): $(BUILD_DIR)\$(PHPDLL) -$(BUILD_DIR) $(BUILD_DIRS_SUB) $(BUILD_DIR_DEV): +$(BUILD_DIR_TARGET): @echo Recreating build dirs @if not exist $(BUILD_DIR) mkdir $(BUILD_DIR) @cd $(BUILD_DIR) diff --git a/win32/build/config.w32 b/win32/build/config.w32 index 012c52499b64..c22e35ce35f4 100644 --- a/win32/build/config.w32 +++ b/win32/build/config.w32 @@ -62,13 +62,18 @@ DEFINE('PHP_PREFIX', PHP_PREFIX); DEFINE("BASE_INCLUDES", "/I . /I main /I Zend /I TSRM /I ext "); +ARG_ENABLE('parallel-build', 'Generate independent object targets for a parallel NMAKE-compatible tool', 'no'); +DEFINE('PARALLEL_BUILD', PHP_PARALLEL_BUILD); + toolset_setup_common_cflags(); if (VS_TOOLSET) { ARG_WITH('mp', 'Tell Visual Studio use up to [n,auto,disable] processes for compilation', 'auto'); var PHP_MP_DISABLED = true; - if (PHP_MP != 'disable') { + if (is_parallel_build_enabled()) { + STDOUT.WriteLine('Using make-level parallel compilation'); + } else if (PHP_MP != 'disable') { if(PHP_DEBUG == 'yes') { STDOUT.WriteLine('WARNING: Debug builds cannot be built using multi processing'); } else { @@ -273,14 +278,16 @@ if (TARGET_ARCH == 'x64') { } ADD_FLAG('ASM_OBJS', all_asm_objs); -MFO.WriteLine('$(BUILD_DIR)\\Zend\\jump_' + FIBER_ASM_ABI + '.obj: Zend\\asm\\jump_' + FIBER_ASM_ABI + '.asm'); +var fiber_asm_build_deps = is_parallel_build_enabled() ? 'generated_files ' : ''; + +MFO.WriteLine('$(BUILD_DIR)\\Zend\\jump_' + FIBER_ASM_ABI + '.obj: ' + fiber_asm_build_deps + 'Zend\\asm\\jump_' + FIBER_ASM_ABI + '.asm'); MFO.WriteLine('\t$(PHP_ASSEMBLER) $(FIBER_ASM_FLAGS) $(BUILD_DIR)\\Zend\\jump_$(FIBER_ASM_ABI).obj Zend\\asm\\jump_$(FIBER_ASM_ABI).asm'); -MFO.WriteLine('$(BUILD_DIR)\\Zend\\make_' + FIBER_ASM_ABI + '.obj: Zend\\asm\\make_' + FIBER_ASM_ABI + '.asm'); +MFO.WriteLine('$(BUILD_DIR)\\Zend\\make_' + FIBER_ASM_ABI + '.obj: ' + fiber_asm_build_deps + 'Zend\\asm\\make_' + FIBER_ASM_ABI + '.asm'); MFO.WriteLine('\t$(PHP_ASSEMBLER) $(FIBER_ASM_FLAGS) $(BUILD_DIR)\\Zend\\make_$(FIBER_ASM_ABI).obj Zend\\asm\\make_$(FIBER_ASM_ABI).asm'); if (TARGET_ARCH == 'x64') { - MFO.WriteLine('$(BUILD_DIR)\\Zend\\save_xmm_x86_64_ms_masm.obj: Zend\\asm\\save_xmm_x86_64_ms_masm.asm'); + MFO.WriteLine('$(BUILD_DIR)\\Zend\\save_xmm_x86_64_ms_masm.obj: ' + fiber_asm_build_deps + 'Zend\\asm\\save_xmm_x86_64_ms_masm.asm'); MFO.WriteLine('\t$(PHP_ASSEMBLER) $(FIBER_ASM_FLAGS) $(BUILD_DIR)\\Zend\\save_xmm_x86_64_ms_masm.obj Zend\\asm\\save_xmm_x86_64_ms_masm.asm'); } diff --git a/win32/build/confutils.js b/win32/build/confutils.js index 7d9297e8c2d6..edbac02d933a 100644 --- a/win32/build/confutils.js +++ b/win32/build/confutils.js @@ -1109,16 +1109,23 @@ function CHECK_HEADER_ADD_INCLUDE(header_name, flag_name, path_to_check, use_env return p; } +function is_parallel_build_enabled() +{ + return typeof PHP_PARALLEL_BUILD != "undefined" && PHP_PARALLEL_BUILD == "yes"; +} + /* XXX check whether some manifest was originally supplied, otherwise keep using the default. */ function generate_version_info_manifest(makefiletarget) { var manifest_name = makefiletarget + ".manifest"; + var build_deps = !MODE_PHPIZE && is_parallel_build_enabled() + ? "generated_files " : ""; if (MODE_PHPIZE) { MFO.WriteLine("$(BUILD_DIR)\\" + manifest_name + ": " + PHP_DIR + "\\build\\default.manifest"); MFO.WriteLine("\t" + CMD_MOD2 + "copy " + PHP_DIR + "\\build\\default.manifest $(BUILD_DIR)\\" + makefiletarget + ".manifest >nul"); } else { - MFO.WriteLine("$(BUILD_DIR)\\" + manifest_name + ": win32\\build\\default.manifest"); + MFO.WriteLine("$(BUILD_DIR)\\" + manifest_name + ": " + build_deps + "win32\\build\\default.manifest"); MFO.WriteLine("\t" + CMD_MOD2 + "copy $(PHP_SRC_DIR)\\win32\\build\\default.manifest $(BUILD_DIR)\\" + makefiletarget + ".manifest >nul"); } @@ -1140,6 +1147,8 @@ function generate_version_info_resource(makefiletarget, basename, creditspath, s var project_url = "https://www.php.net"; var project_header = creditspath + "/php_" + basename + ".h"; var versioning = ""; + var build_deps = !MODE_PHPIZE && is_parallel_build_enabled() + ? "generated_files " : ""; if (sapi) { var internal_name = basename.toUpperCase() + " SAPI"; @@ -1197,7 +1206,7 @@ function generate_version_info_resource(makefiletarget, basename, creditspath, s * Use user supplied template.rc if it exists */ if (FSO.FileExists(creditspath + '\\template.rc')) { - MFO.WriteLine("$(BUILD_DIR)\\" + resname + ": " + creditspath + "\\template.rc"); + MFO.WriteLine("$(BUILD_DIR)\\" + resname + ": " + build_deps + creditspath + "\\template.rc"); MFO.WriteLine("\t" + CMD_MOD1 + "$(RC) /nologo $(BASE_INCLUDES) /fo $(BUILD_DIR)\\" + resname + logo + debug + ' /d FILE_DESCRIPTION="\\"' + res_desc + '\\"" /d FILE_NAME="\\"' + makefiletarget + '\\"" /d PRODUCT_NAME="\\"' + res_prod_name + @@ -1213,7 +1222,7 @@ function generate_version_info_resource(makefiletarget, basename, creditspath, s '\\"" /d INTERNAL_NAME="\\"' + internal_name + versioning + '\\"" /d THANKS_GUYS="\\"' + thanks + '\\"" $(PHP_DIR)\\build\\template.rc'); } else { - MFO.WriteLine("$(BUILD_DIR)\\" + resname + ": win32\\build\\template.rc"); + MFO.WriteLine("$(BUILD_DIR)\\" + resname + ": " + build_deps + "win32\\build\\template.rc"); MFO.WriteLine("\t" + CMD_MOD1 + "$(RC) /nologo $(BASE_INCLUDES) /n /fo $(BUILD_DIR)\\" + resname + logo + debug + ' /d FILE_DESCRIPTION="\\"' + res_desc + '\\"" /d FILE_NAME="\\"' + makefiletarget + '\\"" /d URL="\\"' + project_url + @@ -1613,6 +1622,7 @@ function ADD_SOURCES(dir, file_list, target, obj_dir, duplicate_sources) var i; var tv; var src, obj, sym, flags; + var parallel_build = is_parallel_build_enabled(); if (target == null) { target = "php"; @@ -1712,7 +1722,7 @@ function ADD_SOURCES(dir, file_list, target, obj_dir, duplicate_sources) srcs_by_dir[build_dir].push(i); } - if (!duplicate_sources) { + if (!duplicate_sources && !parallel_build) { /* Create makefile build targets and dependencies. */ MFO.WriteLine(objs_line + ": " + srcs_line); } @@ -1730,7 +1740,7 @@ function ADD_SOURCES(dir, file_list, target, obj_dir, duplicate_sources) var mangle_dir = k.replace(new RegExp("[\\\\/.-]", "g"), "_"); var bd_flags_name = "CFLAGS_BD_" + mangle_dir.toUpperCase(); - if (VS_TOOLSET) { + if (VS_TOOLSET && !parallel_build) { ADD_FLAG(bd_flags_name, "/Fd" + sub_build + d); } @@ -1797,7 +1807,36 @@ function ADD_SOURCES(dir, file_list, target, obj_dir, duplicate_sources) } if (!duplicate_sources) { - if (PHP_MP_DISABLED) { + if (parallel_build) { + for (var j in srcs_by_dir[k]) { + src = file_list[srcs_by_dir[k][j]]; + + var _tmp = src.split("\\"); + var filename = _tmp.pop(); + obj = sub_build + d + filename.replace(re, ".obj"); + + /* A parallel make needs one recipe owner per output. Generated files are + * an explicit prerequisite instead of relying on nmake's serial order. */ + MFO.WriteLine(obj + ": generated_files " + dir + "\\" + src); + + var pdb_flag = ""; + if (VS_TOOLSET) { + /* Concurrent cl.exe processes must not share a PDB/IDB file. */ + pdb_flag = " /Fd" + sub_build + d + filename.replace(re, ".pdb"); + } + + MFO.WriteLine("\t" + CMD_MOD1 + "$(CC) $(" + flags + ") $(CFLAGS) $(" + bd_flags_name + ")" + pdb_flag + " /c " + dir + "\\" + src + " /Fo" + obj); + + if ("clang" == PHP_ANALYZER) { + MFO.WriteLine("\t" + CMD_MOD1 + "\"$(CLANG_CL)\" " + analyzer_base_args + " $(" + flags + "_ANALYZER) $(CFLAGS_ANALYZER) $(" + bd_flags_name + "_ANALYZER) " + dir + "\\" + src); + } else if ("cppcheck" == PHP_ANALYZER) { + MFO.WriteLine("\t\"" + CMD_MOD1 + "$(CPPCHECK)\" " + analyzer_base_args + " $(" + flags + "_ANALYZER) $(CFLAGS_ANALYZER) $(" + bd_flags_name + "_ANALYZER) " + analyzer_base_flags + " " + dir + "\\" + src); + } else if (PHP_ANALYZER == "pvs") { + MFO.WriteLine("\t" + CMD_MOD1 + "\"$(PVS_STUDIO)\" --cl-params $(" + flags + ") $(CFLAGS) $(" + bd_flags_name + ")" + pdb_flag + " /c " + dir + "\\" + src + " --source-file " + dir + "\\" + src + + " --cfg PVS-Studio.conf --errors-off \"V122 V117 V111\" "); + } + } + } else if (PHP_MP_DISABLED) { for (var j in srcs_by_dir[k]) { src = file_list[srcs_by_dir[k][j]]; @@ -3322,7 +3361,12 @@ function toolset_setup_common_cflags() } if (VS_TOOLSET) { - ADD_FLAG("CFLAGS", " /FD "); + /* /FD is only used by the Visual Studio IDE and prevents sccache from + * recognizing command-line builds. Parallel builds already use a unique + * /Fd program database for every object. */ + if (!is_parallel_build_enabled()) { + ADD_FLAG("CFLAGS", " /FD "); + } // fun stuff: MS deprecated ANSI stdio and similar functions // disable annoying warnings. In addition, time_t defaults @@ -3356,7 +3400,9 @@ function toolset_setup_common_cflags() /* This is only in effect for CXX sources, __cplusplus is not defined in C sources. */ ADD_FLAG("CFLAGS", "/Zc:__cplusplus"); - ADD_FLAG("CFLAGS", "/d2FuncCache1"); + /* MSVC accepts '-' as an option prefix. The slash-prefixed lowercase + * form is otherwise mistaken for an input path by sccache. */ + ADD_FLAG("CFLAGS", "-d2FuncCache1"); if (VCVERS >= 1930) { ADD_FLAG("CFLAGS", "/Zc:preprocessor"); From 8e526f112300e62364b4cc66f947284c90696332 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Mon, 3 Aug 2026 01:41:15 +0700 Subject: [PATCH 2/6] perf: speed up benchmark checkout --- .github/workflows/test-suite.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index e7bd767005da..3d12fcc15d10 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -941,6 +941,7 @@ jobs: with: ref: ${{ fromJson(inputs.branch).ref }} fetch-depth: 0 + filter: blob:none # ASLR can cause a lot of noise due to missed sse opportunities for memcpy # and other operations, so we disable it during benchmarking. - name: Disable ASLR From d2b0d08ee0700710b31d6bcb5aafcaa3d5404106 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Mon, 3 Aug 2026 01:54:53 +0700 Subject: [PATCH 3/6] perf: keep non-Windows compiler caches fresh --- .github/actions/ccache/action.yml | 8 ++++---- .github/workflows/unit-tests.yml | 9 ++++----- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/actions/ccache/action.yml b/.github/actions/ccache/action.yml index a73a6cbe1e7d..2bc47d69895f 100644 --- a/.github/actions/ccache/action.yml +++ b/.github/actions/ccache/action.yml @@ -21,12 +21,12 @@ runs: major=$(cat ${{ inputs.php_directory }}/main/php_version.h | sed -En 's/^#define PHP_MAJOR_VERSION ([0-9]+)/\1/p') minor=$(cat ${{ inputs.php_directory }}/main/php_version.h | sed -En 's/^#define PHP_MINOR_VERSION ([0-9]+)/\1/p') release=$(cat ${{ inputs.php_directory }}/main/php_version.h | sed -En 's/^#define PHP_RELEASE_VERSION ([0-9]+)/\1/p') - week=$(date +"%Y-%W") prefix="${{ inputs.name }}-$major.$minor.$release" - echo "key=$prefix-$week" >> $GITHUB_OUTPUT - echo "prefix=$prefix-" >> $GITHUB_OUTPUT + # GitHub cache entries are immutable, so each commit needs a new key. + echo "key=$prefix-$GITHUB_SHA" >> $GITHUB_OUTPUT + echo "prefix=$prefix" >> $GITHUB_OUTPUT - name: ccache - uses: hendrikmuhs/ccache-action@v1.2 + uses: hendrikmuhs/ccache-action@d62db5f07c26379fc4b4e0916f098a92573c3b03 # v1.2.23 with: key: "${{ steps.cache_key.outputs.key }}" append-timestamp: false diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index fa79580f42ec..6e1efe979a80 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -6,6 +6,7 @@ on: - 'main/io/**' - 'main/php_io.h' - 'tests/unit/**' + - '.github/actions/ccache/action.yml' - '.github/workflows/unit-tests.yml' branches: - master @@ -15,6 +16,7 @@ on: - 'main/io/**' - 'main/php_io.h' - 'tests/unit/**' + - '.github/actions/ccache/action.yml' - '.github/workflows/unit-tests.yml' branches: - '**' @@ -57,11 +59,9 @@ jobs: ccache - name: ccache - uses: hendrikmuhs/ccache-action@v1.2 + uses: ./.github/actions/ccache with: - key: "unit-tests-${{hashFiles('main/php_version.h')}}" - append-timestamp: false - save: ${{ github.event_name != 'pull_request' }} + name: UNIT_TESTS_LINUX_X64 - name: ./configure (minimal build) uses: ./.github/actions/configure-unit-tests @@ -76,4 +76,3 @@ jobs: set -x cd tests/unit make test - From 9de2a2e3e1875518a3b66eaaf06b131d66000704 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Mon, 3 Aug 2026 04:18:10 +0700 Subject: [PATCH 4/6] perf: skip unused ASAN setup --- .github/actions/apt-x64/action.yml | 30 ++++++++++++------------------ .github/workflows/test-suite.yml | 6 ++++-- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/.github/actions/apt-x64/action.yml b/.github/actions/apt-x64/action.yml index 3e654ef9fdc4..baac80d23ae2 100644 --- a/.github/actions/apt-x64/action.yml +++ b/.github/actions/apt-x64/action.yml @@ -3,6 +3,9 @@ inputs: asan: default: false required: false + skipSlow: + default: false + required: false runs: using: composite steps: @@ -11,6 +14,13 @@ runs: set -x OPCACHE_TLS_TESTS_DEPS="gcc clang lld" + SLOW_EXTENSION_DEPS="" + if [[ "${{ inputs.skipSlow }}" == "false" ]]; then + SLOW_EXTENSION_DEPS="ldap-utils slapd libtidy-dev libenchant-2-dev libsasl2-dev" + SLOW_EXTENSION_DEPS+=" libsqlite3-mod-spatialite libpq-dev libldap2-dev libsnmp-dev" + SLOW_EXTENSION_DEPS+=" postgresql postgresql-contrib snmpd snmp-mibs-downloader" + SLOW_EXTENSION_DEPS+=" freetds-dev unixodbc-dev libsqliteodbc firebird-dev" + fi export DEBIAN_FRONTEND=noninteractive @@ -30,47 +40,30 @@ runs: bison \ re2c \ locales \ - ldap-utils \ openssl \ - slapd \ language-pack-de \ libgmp-dev \ libicu-dev \ - libtidy-dev \ - libenchant-2-dev \ libbz2-dev \ - libsasl2-dev \ libxpm-dev \ libzip-dev \ libsqlite3-dev \ - libsqlite3-mod-spatialite \ libwebp-dev \ ${{ inputs.asan == 'false' && 'libavif-dev' || '' }} \ libonig-dev \ libcurl4-openssl-dev \ libxml2-dev \ libxslt1-dev \ - libpq-dev \ libedit-dev \ - libldap2-dev \ libsodium-dev \ libargon2-dev \ libmm-dev \ - libsnmp-dev \ - postgresql \ - postgresql-contrib \ - snmpd \ - snmp-mibs-downloader \ - freetds-dev \ - unixodbc-dev \ - libsqliteodbc \ llvm \ clang \ dovecot-core \ dovecot-pop3d \ dovecot-imapd \ sendmail \ - firebird-dev \ liblmdb-dev \ libtokyocabinet-dev \ libdb-dev \ @@ -78,4 +71,5 @@ runs: libjpeg-dev \ libpng-dev \ libfreetype6-dev \ - $OPCACHE_TLS_TESTS_DEPS + $OPCACHE_TLS_TESTS_DEPS \ + $SLOW_EXTENSION_DEPS diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 3d12fcc15d10..9a7a638d611d 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -113,13 +113,13 @@ jobs: MYSQL_DATABASE: test MYSQL_ROOT_PASSWORD: root postgres: - image: postgres + image: ${{ (!matrix.asan || inputs.all_variations) && 'postgres' || '' }} env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: test firebird: - image: jacobalberty/firebird + image: ${{ (!matrix.asan || inputs.all_variations) && 'jacobalberty/firebird' || '' }} ports: - 3050:3050 env: @@ -139,11 +139,13 @@ jobs: with: ref: ${{ fromJson(inputs.branch).ref }} - name: Create MSSQL container + if: ${{ !matrix.asan || inputs.all_variations }} uses: ./.github/actions/setup-mssql - name: apt uses: ./.github/actions/apt-x64 with: asan: ${{ matrix.asan && 'true' || 'false' }} + skipSlow: ${{ (matrix.asan && !inputs.all_variations) && 'true' || 'false' }} - name: System info run: | echo "::group::Show host CPU info" From 75076295b4c1cf4862be1a3a6eb55eae8dcb585b Mon Sep 17 00:00:00 2001 From: NickSdot Date: Mon, 3 Aug 2026 14:19:55 +0700 Subject: [PATCH 5/6] perf: run Function JIT tests concurrently --- .github/actions/ccache/action.yml | 5 ++++- .github/matrix.php | 5 +++-- .github/workflows/test-suite.yml | 15 ++++++++++----- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/.github/actions/ccache/action.yml b/.github/actions/ccache/action.yml index 2bc47d69895f..aa9b8b267d2c 100644 --- a/.github/actions/ccache/action.yml +++ b/.github/actions/ccache/action.yml @@ -11,6 +11,9 @@ inputs: cxx: required: false default: 'g++' + save: + required: false + default: true runs: using: composite steps: @@ -31,7 +34,7 @@ runs: key: "${{ steps.cache_key.outputs.key }}" append-timestamp: false restore-keys: "${{ steps.cache_key.outputs.prefix }}" - save: ${{ github.event_name != 'pull_request' }} + save: ${{ inputs.save == 'true' && github.event_name != 'pull_request' }} - name: Export CC/CXX shell: bash run: | diff --git a/.github/matrix.php b/.github/matrix.php index 7970442d7050..7204d9743bbc 100644 --- a/.github/matrix.php +++ b/.github/matrix.php @@ -113,8 +113,9 @@ function select_jobs($repository, $trigger, $nightly, $labels, $php_version, $re ], ] : ['include' => [ - ['name' => '', 'asan' => false, 'debug' => false, 'repeat' => false, 'variation' => false, 'zts' => false], - ['name' => '_ASAN', 'asan' => true, 'debug' => true, 'repeat' => false, 'variation' => false, 'zts' => true], + ['name' => '', 'asan' => false, 'debug' => false, 'repeat' => false, 'test_mode' => 'normal', 'variation' => false, 'zts' => false], + ['name' => '', 'asan' => false, 'debug' => false, 'repeat' => false, 'test_mode' => 'function-jit', 'variation' => false, 'zts' => false], + ['name' => '_ASAN', 'asan' => true, 'debug' => true, 'repeat' => false, 'test_mode' => 'normal', 'variation' => false, 'zts' => true], ]]; $jobs['LINUX_X64']['config']['variation_enable_zend_max_execution_timers'] = version_compare($php_version, '8.3', '>='); } diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 9a7a638d611d..c225569e07d2 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -130,7 +130,7 @@ jobs: strategy: fail-fast: false matrix: ${{ fromJson(inputs.branch).jobs.LINUX_X64.matrix }} - name: "LINUX_X64${{ matrix.name }}_${{ matrix.debug && 'DEBUG' || 'RELEASE' }}_${{ matrix.zts && 'ZTS' || 'NTS' }}" + name: "LINUX_X64${{ matrix.name }}_${{ matrix.debug && 'DEBUG' || 'RELEASE' }}_${{ matrix.zts && 'ZTS' || 'NTS' }}${{ matrix.test_mode == 'function-jit' && '_FUNCTION_JIT' || '' }}" runs-on: ubuntu-${{ fromJson(inputs.branch).config.ubuntu_version }} timeout-minutes: 180 steps: @@ -163,6 +163,9 @@ jobs: # GitHub has no way to query the job name (github.job is the # job id, not the job name) name: "LINUX_X64${{ matrix.name }}_${{ matrix.debug && 'DEBUG' || 'RELEASE' }}_${{ matrix.zts && 'ZTS' || 'NTS' }}" + # Both test modes build the same configuration and share a cache. + # Only the normal job saves it to avoid concurrent writes. + save: ${{ matrix.test_mode != 'function-jit' && 'true' || 'false' }} - name: ./configure uses: ./.github/actions/configure-x64 with: @@ -183,7 +186,7 @@ jobs: if: ${{ !matrix.asan || inputs.all_variations }} uses: ./.github/actions/setup-x64 - name: Test - if: ${{ inputs.all_variations || !matrix.asan }} + if: ${{ (inputs.all_variations || !matrix.asan) && matrix.test_mode != 'function-jit' }} uses: ./.github/actions/test-linux with: runTestsParameters: >- @@ -192,7 +195,7 @@ jobs: ${{ matrix.variation && '-d zend_test.observer.enabled=1 -d zend_test.observer.show_output=0' || '' }} idleCpu: ${{ matrix.asan && 'true' || 'false' }} - name: Test Tracing JIT - if: ${{ inputs.all_variations || matrix.asan }} + if: ${{ (inputs.all_variations || matrix.asan) && matrix.test_mode != 'function-jit' }} uses: ./.github/actions/test-linux with: enableOpcache: true @@ -203,7 +206,7 @@ jobs: ${{ matrix.repeat && '--repeat 2' || '' }} ${{ matrix.variation && '-d zend_test.observer.enabled=1 -d zend_test.observer.show_output=0' || '' }} - name: Test OpCache - if: ${{ inputs.all_variations }} + if: ${{ inputs.all_variations && matrix.test_mode != 'function-jit' }} uses: ./.github/actions/test-linux with: enableOpcache: true @@ -214,7 +217,7 @@ jobs: - name: Test Function JIT # ASAN frequently timeouts. Each test run takes ~90 minutes, we can # avoid running into the 6 hour timeout by skipping the function JIT. - if: ${{ !matrix.asan }} + if: ${{ !matrix.asan && matrix.test_mode != 'normal' }} uses: ./.github/actions/test-linux with: enableOpcache: true @@ -223,8 +226,10 @@ jobs: ${{ matrix.repeat && '--repeat 2' || '' }} ${{ matrix.variation && '-d zend_test.observer.enabled=1 -d zend_test.observer.show_output=0' || '' }} - name: Extra tests + if: ${{ matrix.test_mode != 'function-jit' }} uses: ./.github/actions/extra-tests - name: Verify generated files are up to date + if: ${{ matrix.test_mode != 'function-jit' }} uses: ./.github/actions/verify-generated-files LINUX_X32: if: ${{ fromJson(inputs.branch).jobs.LINUX_X32 }} From b94064e393fa290ce8d58199e3ad8c412d87b443 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Mon, 3 Aug 2026 16:04:30 +0700 Subject: [PATCH 6/6] perf: shard long running jobs --- .github/actions/test-alpine/action.yml | 15 ++++- .github/actions/test-linux/action.yml | 15 ++++- .github/matrix.php | 25 ++++++-- .github/scripts/generate_test_shard.php | 82 +++++++++++++++++++++++++ .github/scripts/windows/test_task.bat | 9 ++- .github/workflows/test-suite.yml | 29 ++++++--- 6 files changed, 159 insertions(+), 16 deletions(-) create mode 100644 .github/scripts/generate_test_shard.php diff --git a/.github/actions/test-alpine/action.yml b/.github/actions/test-alpine/action.yml index 5911c6ce57f2..507d697c4048 100644 --- a/.github/actions/test-alpine/action.yml +++ b/.github/actions/test-alpine/action.yml @@ -9,6 +9,12 @@ inputs: jitType: default: 'disable' required: false + testShard: + default: 1 + required: false + testShards: + default: 1 + required: false runs: using: composite steps: @@ -17,6 +23,12 @@ runs: set -x export SKIP_IO_CAPTURE_TESTS=1 export STACK_LIMIT_DEFAULTS_CHECK=1 + test_shard_options=() + if [[ "${{ inputs.testShards }}" -gt 1 ]]; then + sapi/cli/php .github/scripts/generate_test_shard.php \ + "${{ inputs.testShard }}" "${{ inputs.testShards }}" "$RUNNER_TEMP/php-test-shard.txt" + test_shard_options=(-r "$RUNNER_TEMP/php-test-shard.txt") + fi sapi/cli/php run-tests.php -P -q ${{ inputs.runTestsParameters }} \ ${{ inputs.enableOpcache == 'true' && '-d opcache.enable_cli=1' || '' }} \ -d opcache.jit=${{ inputs.jitType }} \ @@ -26,4 +38,5 @@ runs: --no-progress \ --show-diff \ --show-slow 1000 \ - --set-timeout 120 + --set-timeout 120 \ + "${test_shard_options[@]}" diff --git a/.github/actions/test-linux/action.yml b/.github/actions/test-linux/action.yml index e89c942d52e1..9d7e749aba1b 100644 --- a/.github/actions/test-linux/action.yml +++ b/.github/actions/test-linux/action.yml @@ -12,6 +12,12 @@ inputs: idleCpu: default: 'false' required: false + testShard: + default: 1 + required: false + testShards: + default: 1 + required: false runs: using: composite steps: @@ -45,6 +51,12 @@ runs: export SKIP_IO_CAPTURE_TESTS=1 export STACK_LIMIT_DEFAULTS_CHECK=1 export RUN_RESOURCE_HEAVY_TESTS=1 + test_shard_options=() + if [[ "${{ inputs.testShards }}" -gt 1 ]]; then + sapi/cli/php .github/scripts/generate_test_shard.php \ + "${{ inputs.testShard }}" "${{ inputs.testShards }}" "$RUNNER_TEMP/php-test-shard.txt" + test_shard_options=(-r "$RUNNER_TEMP/php-test-shard.txt") + fi sapi/cli/php run-tests.php -P -q ${{ inputs.runTestsParameters }} \ ${{ inputs.enableOpcache == 'true' && '-d opcache.enable_cli=1' || '' }} \ -d opcache.jit=${{ inputs.jitType }} \ @@ -56,4 +68,5 @@ runs: --offline \ --show-diff \ --show-slow 1000 \ - --set-timeout 120 + --set-timeout 120 \ + "${test_shard_options[@]}" diff --git a/.github/matrix.php b/.github/matrix.php index 7204d9743bbc..a9ad7f5cee6b 100644 --- a/.github/matrix.php +++ b/.github/matrix.php @@ -46,6 +46,17 @@ function get_current_version(): array { return [$major, $minor]; } +function with_test_shards(array $configuration, int $shards): array { + $configurations = []; + for ($shard = 1; $shard <= $shards; $shard++) { + $configurations[] = $configuration + [ + 'test_shard' => $shard, + 'test_shards' => $shards, + ]; + } + return $configurations; +} + function select_jobs($repository, $trigger, $nightly, $labels, $php_version, $ref, $all_variations) { $no_jobs = in_array('CI: No jobs', $labels, true); $all_jobs = in_array('CI: All jobs', $labels, true) || $nightly; @@ -66,7 +77,7 @@ function select_jobs($repository, $trigger, $nightly, $labels, $php_version, $re $jobs = []; if (version_compare($php_version, '8.4', '>=') && ($all_jobs || !$no_jobs || $test_alpine)) { - $jobs['ALPINE'] = true; + $jobs['ALPINE']['matrix']['include'] = with_test_shards([], $all_variations ? 1 : 3); } if (version_compare($php_version, '8.4', '>=') && !$nightly @@ -113,9 +124,9 @@ function select_jobs($repository, $trigger, $nightly, $labels, $php_version, $re ], ] : ['include' => [ - ['name' => '', 'asan' => false, 'debug' => false, 'repeat' => false, 'test_mode' => 'normal', 'variation' => false, 'zts' => false], - ['name' => '', 'asan' => false, 'debug' => false, 'repeat' => false, 'test_mode' => 'function-jit', 'variation' => false, 'zts' => false], - ['name' => '_ASAN', 'asan' => true, 'debug' => true, 'repeat' => false, 'test_mode' => 'normal', 'variation' => false, 'zts' => true], + ...with_test_shards(['name' => '', 'asan' => false, 'debug' => false, 'repeat' => false, 'test_mode' => 'normal', 'variation' => false, 'zts' => false], 1), + ...with_test_shards(['name' => '', 'asan' => false, 'debug' => false, 'repeat' => false, 'test_mode' => 'function-jit', 'variation' => false, 'zts' => false], 1), + ...with_test_shards(['name' => '_ASAN', 'asan' => true, 'debug' => true, 'repeat' => false, 'test_mode' => 'normal', 'variation' => false, 'zts' => true], 3), ]]; $jobs['LINUX_X64']['config']['variation_enable_zend_max_execution_timers'] = version_compare($php_version, '8.3', '>='); } @@ -148,7 +159,13 @@ function select_jobs($repository, $trigger, $nightly, $labels, $php_version, $re if (version_compare($php_version, '8.6', '>=')) { $matrix[] = ['asan' => false, 'opcache' => true, 'x64' => true, 'zts' => true, 'clang' => true]; } + } else { + $matrix = with_test_shards($matrix[0], 2); + } + foreach ($matrix as &$configuration) { + $configuration += ['test_shard' => 1, 'test_shards' => 1]; } + unset($configuration); $jobs['WINDOWS']['matrix'] = ['include' => $matrix]; $jobs['WINDOWS']['config'] = match (true) { version_compare($php_version, '8.6', '>=') => ['vs_crt_version' => 'vs18', 'runs_on' => 'windows-2025-vs2026'], diff --git a/.github/scripts/generate_test_shard.php b/.github/scripts/generate_test_shard.php new file mode 100644 index 000000000000..dbf6770d4cc6 --- /dev/null +++ b/.github/scripts/generate_test_shard.php @@ -0,0 +1,82 @@ + \n"); + exit(1); +} + +$shard = filter_var($argv[1], FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]); +$shards = filter_var($argv[2], FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]); +if ($shard === false || $shards === false || $shard > $shards) { + fwrite(STDERR, "The shard must be between 1 and the total number of shards.\n"); + exit(1); +} + +$root = dirname(__DIR__, 2); +$extensions = get_loaded_extensions(); +$extensionDir = ini_get('extension_dir'); +if (is_dir($extensionDir)) { + foreach (scandir($extensionDir) as $file) { + if (preg_match('/^(?:php_)?([_a-zA-Z0-9]+)\.' . preg_quote(PHP_SHLIB_SUFFIX, '/') . '$/', $file, $matches)) { + $extensions[] = $matches[1]; + } + } +} +$extensions = array_map( + static fn(string $extension): string => $extension === 'Zend OPcache' ? 'opcache' : strtolower($extension), + $extensions, +); +$extensions = array_fill_keys($extensions, true); +unset($extensions['core']); + +$tests = []; +$collectTests = static function (string $directory) use (&$tests): void { + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($directory, FilesystemIterator::SKIP_DOTS), + ); + foreach ($iterator as $file) { + if ($file->isFile() && $file->getExtension() === 'phpt' && $file->getFilename()[0] !== '.') { + $tests[] = $file->getRealPath(); + } + } +}; + +foreach (['Zend', 'tests', 'sapi'] as $directory) { + if (is_dir("$root/$directory")) { + $collectTests("$root/$directory"); + } +} +foreach (new DirectoryIterator("$root/ext") as $extensionDirectory) { + if ($extensionDirectory->isDir() + && !$extensionDirectory->isDot() + && isset($extensions[strtolower($extensionDirectory->getFilename())]) + ) { + $collectTests($extensionDirectory->getPathname()); + } +} + +sort($tests, SORT_STRING); +$testShards = array_fill(0, $shards, []); +foreach ($tests as $index => $test) { + $testShards[$index % $shards][] = $test; +} +$partitionedTests = array_merge(...$testShards); +if (count($partitionedTests) !== count($tests) + || count(array_unique($partitionedTests)) !== count($tests) +) { + fwrite(STDERR, "Test shard partition is incomplete or contains duplicates.\n"); + exit(1); +} +$selectedTests = $testShards[$shard - 1]; +if ($selectedTests === []) { + fwrite(STDERR, "Shard $shard of $shards contains no tests.\n"); + exit(1); +} + +$output = $argv[3]; +if (file_put_contents($output, implode(PHP_EOL, $selectedTests) . PHP_EOL) === false) { + fwrite(STDERR, "Failed to write test shard to $output.\n"); + exit(1); +} + +printf("Selected %d of %d tests for shard %d of %d.\n", count($selectedTests), count($tests), $shard, $shards); diff --git a/.github/scripts/windows/test_task.bat b/.github/scripts/windows/test_task.bat index 4ce2bd96ce66..45bd96710cec 100644 --- a/.github/scripts/windows/test_task.bat +++ b/.github/scripts/windows/test_task.bat @@ -149,7 +149,14 @@ if "%ASAN%" equ "1" set ASAN_OPTS=--asan mkdir c:\tests_tmp -nmake test TESTS="%OPCACHE_OPTS% -g FAIL,BORK,LEAK,XLEAK %ASAN_OPTS% --no-progress -q --offline --show-diff --show-slow 1000 --set-timeout 120 --temp-source c:\tests_tmp --temp-target c:\tests_tmp %PARALLEL%" +set TEST_SHARD_OPTS= +if %TEST_SHARDS% GTR 1 ( + %PHP_BUILD_DIR%\php.exe .github\scripts\generate_test_shard.php %TEST_SHARD% %TEST_SHARDS% %RUNNER_TEMP%\php-test-shard.txt + if errorlevel 1 exit /b 3 + set TEST_SHARD_OPTS=-r %RUNNER_TEMP%\php-test-shard.txt +) + +nmake test TESTS="%OPCACHE_OPTS% -g FAIL,BORK,LEAK,XLEAK %ASAN_OPTS% --no-progress -q --offline --show-diff --show-slow 1000 --set-timeout 120 --temp-source c:\tests_tmp --temp-target c:\tests_tmp %PARALLEL% %TEST_SHARD_OPTS%" set EXIT_CODE=%errorlevel% diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index c225569e07d2..d1b73184cc72 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -54,7 +54,10 @@ jobs: uses: ./.github/actions/extra-tests ALPINE: if: ${{ fromJson(inputs.branch).jobs.ALPINE }} - name: ALPINE_X64_ASAN_DEBUG_ZTS + strategy: + fail-fast: false + matrix: ${{ fromJson(inputs.branch).jobs.ALPINE.matrix }} + name: "ALPINE_X64_ASAN_DEBUG_ZTS${{ matrix.test_shards > 1 && format('_SHARD_{0}_OF_{1}', matrix.test_shard, matrix.test_shards) || '' }}" runs-on: ubuntu-24.04 timeout-minutes: 180 container: @@ -80,6 +83,7 @@ jobs: name: "${{ github.job }}" cc: clang-20 cxx: clang++-20 + save: ${{ matrix.test_shard == 1 && 'true' || 'false' }} - name: ./configure uses: ./.github/actions/configure-alpine with: @@ -100,7 +104,10 @@ jobs: jitType: tracing runTestsParameters: >- --asan -x + testShard: ${{ matrix.test_shard }} + testShards: ${{ matrix.test_shards }} - name: Extra tests + if: ${{ matrix.test_shard == 1 }} uses: ./.github/actions/extra-tests LINUX_X64: if: ${{ fromJson(inputs.branch).jobs.LINUX_X64 }} @@ -130,7 +137,7 @@ jobs: strategy: fail-fast: false matrix: ${{ fromJson(inputs.branch).jobs.LINUX_X64.matrix }} - name: "LINUX_X64${{ matrix.name }}_${{ matrix.debug && 'DEBUG' || 'RELEASE' }}_${{ matrix.zts && 'ZTS' || 'NTS' }}${{ matrix.test_mode == 'function-jit' && '_FUNCTION_JIT' || '' }}" + name: "LINUX_X64${{ matrix.name }}_${{ matrix.debug && 'DEBUG' || 'RELEASE' }}_${{ matrix.zts && 'ZTS' || 'NTS' }}${{ matrix.test_mode == 'function-jit' && '_FUNCTION_JIT' || '' }}${{ matrix.test_shards > 1 && format('_SHARD_{0}_OF_{1}', matrix.test_shard, matrix.test_shards) || '' }}" runs-on: ubuntu-${{ fromJson(inputs.branch).config.ubuntu_version }} timeout-minutes: 180 steps: @@ -163,9 +170,9 @@ jobs: # GitHub has no way to query the job name (github.job is the # job id, not the job name) name: "LINUX_X64${{ matrix.name }}_${{ matrix.debug && 'DEBUG' || 'RELEASE' }}_${{ matrix.zts && 'ZTS' || 'NTS' }}" - # Both test modes build the same configuration and share a cache. - # Only the normal job saves it to avoid concurrent writes. - save: ${{ matrix.test_mode != 'function-jit' && 'true' || 'false' }} + # Test modes and shards of the same configuration share a cache. + # Only the normal job's first shard saves it to avoid concurrent writes. + save: ${{ (matrix.test_mode != 'function-jit' && (!matrix.test_shard || matrix.test_shard == 1)) && 'true' || 'false' }} - name: ./configure uses: ./.github/actions/configure-x64 with: @@ -205,6 +212,8 @@ jobs: ${{ (matrix.asan && !inputs.all_variations) && '-x' || '' }} ${{ matrix.repeat && '--repeat 2' || '' }} ${{ matrix.variation && '-d zend_test.observer.enabled=1 -d zend_test.observer.show_output=0' || '' }} + testShard: ${{ matrix.test_shard || 1 }} + testShards: ${{ matrix.test_shards || 1 }} - name: Test OpCache if: ${{ inputs.all_variations && matrix.test_mode != 'function-jit' }} uses: ./.github/actions/test-linux @@ -226,10 +235,10 @@ jobs: ${{ matrix.repeat && '--repeat 2' || '' }} ${{ matrix.variation && '-d zend_test.observer.enabled=1 -d zend_test.observer.show_output=0' || '' }} - name: Extra tests - if: ${{ matrix.test_mode != 'function-jit' }} + if: ${{ matrix.test_mode != 'function-jit' && (!matrix.test_shard || matrix.test_shard == 1) }} uses: ./.github/actions/extra-tests - name: Verify generated files are up to date - if: ${{ matrix.test_mode != 'function-jit' }} + if: ${{ matrix.test_mode != 'function-jit' && (!matrix.test_shard || matrix.test_shard == 1) }} uses: ./.github/actions/verify-generated-files LINUX_X32: if: ${{ fromJson(inputs.branch).jobs.LINUX_X32 }} @@ -852,7 +861,7 @@ jobs: strategy: fail-fast: false matrix: ${{ fromJson(inputs.branch).jobs.WINDOWS.matrix }} - name: "WINDOWS_${{ matrix.x64 && 'X64' || 'X86' }}_${{ matrix.zts && 'ZTS' || 'NTS' }}${{ matrix.asan && '_ASAN' || ''}}${{ matrix.clang && '_CLANG' || ''}}" + name: "WINDOWS_${{ matrix.x64 && 'X64' || 'X86' }}_${{ matrix.zts && 'ZTS' || 'NTS' }}${{ matrix.asan && '_ASAN' || ''}}${{ matrix.clang && '_CLANG' || ''}}${{ matrix.test_shards > 1 && format('_SHARD_{0}_OF_{1}', matrix.test_shard, matrix.test_shards) || '' }}" runs-on: ${{ fromJson(inputs.branch).jobs.WINDOWS.config.runs_on }} timeout-minutes: 180 env: @@ -868,6 +877,8 @@ jobs: OPCACHE: "${{ matrix.opcache && '1' || '0' }}" ASAN: "${{ matrix.asan && '1' || '0' }}" CLANG_TOOLSET: "${{ matrix.clang && '1' || '0' }}" + TEST_SHARD: "${{ matrix.test_shard }}" + TEST_SHARDS: "${{ matrix.test_shards }}" SCCACHE_CACHE_SIZE: 1G SCCACHE_IGNORE_SERVER_IO_ERROR: "1" SCCACHE_CACHE_KEY: windows-sccache-v2-php${{ join(fromJson(inputs.branch).version, '.') }}-${{ fromJson(inputs.branch).jobs.WINDOWS.config.vs_crt_version }}-${{ matrix.x64 && 'x64' || 'x86' }}-zts${{ matrix.zts && '1' || '0' }}-asan${{ matrix.asan && '1' || '0' }}-${{ github.sha }} @@ -895,7 +906,7 @@ jobs: # Pull request caches are isolated to the PR merge ref and cannot update # caches used by the base branch or other pull requests. - name: Save MSVC compiler cache - if: ${{ !matrix.clang }} + if: ${{ !matrix.clang && matrix.test_shard == 1 }} continue-on-error: true uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 with: