diff --git a/src/SPC/builder/linux/LinuxBuilder.php b/src/SPC/builder/linux/LinuxBuilder.php index 770366ff..d15816fa 100644 --- a/src/SPC/builder/linux/LinuxBuilder.php +++ b/src/SPC/builder/linux/LinuxBuilder.php @@ -130,6 +130,7 @@ class LinuxBuilder extends UnixBuilderBase $this->emitPatchPoint('before-php-make'); SourcePatcher::patchBeforeMake($this); + PgoManager::patchBeforeMake($this); $this->cleanMake(); diff --git a/src/SPC/builder/unix/UnixBuilderBase.php b/src/SPC/builder/unix/UnixBuilderBase.php index 2f543bb7..37127f24 100644 --- a/src/SPC/builder/unix/UnixBuilderBase.php +++ b/src/SPC/builder/unix/UnixBuilderBase.php @@ -145,7 +145,7 @@ abstract class UnixBuilderBase extends BuilderBase throw new SPCInternalException("Deploy failed. Cannot find file after copy: {$dst}"); } - if (!$this->getOption('no-strip') && !$this->getOption('pgi')) { + if (!$this->getOption('no-strip') && !$this->getOption('pgi') && !$this->getOption('cs-pgi')) { // extract debug info $this->extractDebugInfo($dst); // extra strip diff --git a/src/SPC/command/BuildPHPCommand.php b/src/SPC/command/BuildPHPCommand.php index 69772efe..9c10b102 100644 --- a/src/SPC/command/BuildPHPCommand.php +++ b/src/SPC/command/BuildPHPCommand.php @@ -51,6 +51,7 @@ class BuildPHPCommand extends BuildCommand $this->addOption('enable-micro-win32', null, null, 'Enable win32 mode for phpmicro (Windows only)'); $this->addOption('with-frankenphp-app', null, InputOption::VALUE_REQUIRED, 'Path to a folder to be embedded in FrankenPHP'); $this->addOption('pgi', null, null, 'Build instrumented binaries (-fprofile-generate). Run them to collect .profraw files, then re-run with --pgo.'); + $this->addOption('cs-pgi', null, null, 'Build cs-instrumented binaries (-fprofile-use= -fcs-profile-generate). Requires a prior --pgi+--pgo cycle.'); $this->addOption('pgo', null, null, 'Build optimised binaries (-fprofile-use) from .profraw collected by a previous --pgi run.'); } @@ -215,13 +216,16 @@ class BuildPHPCommand extends BuildCommand FileSystem::removeDir(BUILD_MODULES_PATH); $pgi = (bool) $this->getOption('pgi'); + $csPgi = (bool) $this->getOption('cs-pgi'); $pgo = (bool) $this->getOption('pgo'); - if ($pgi && $pgo) { - $this->output->writeln('--pgi and --pgo are mutually exclusive'); + if (((int) $pgi + (int) $csPgi + (int) $pgo) > 1) { + $this->output->writeln('--pgi, --cs-pgi, and --pgo are mutually exclusive'); return static::FAILURE; } if ($pgi) { (new PgoManager())->setupInstrument($rule); + } elseif ($csPgi) { + (new PgoManager())->setupCsInstrument($rule); } elseif ($pgo) { (new PgoManager())->setupUse($rule); } diff --git a/src/SPC/store/scripts/zig-cc.sh b/src/SPC/store/scripts/zig-cc.sh index c2b93e8f..586c828c 100644 --- a/src/SPC/store/scripts/zig-cc.sh +++ b/src/SPC/store/scripts/zig-cc.sh @@ -45,11 +45,11 @@ NEED_CRT=0 # https://codeberg.org/ziglang/zig/issues/32064 for _a in "${PARSED_ARGS[@]}"; do case "$_a" in -c|-S|-E|-M|-MM) IS_LINK=0 ;; - -fprofile-generate*|-fprofile-instr-generate*) NEED_PROFILE_RT=1 ;; + -fprofile-generate*|-fprofile-instr-generate*|-fcs-profile-generate*) NEED_PROFILE_RT=1 ;; -shared) NEED_CRT=1 ;; esac done -[[ "$SPC_COMPILER_EXTRA" == *-fprofile-generate* ]] && NEED_PROFILE_RT=1 +[[ "$SPC_COMPILER_EXTRA" == *-fprofile-generate* || "$SPC_COMPILER_EXTRA" == *-fcs-profile-generate* ]] && NEED_PROFILE_RT=1 if [[ $IS_LINK -eq 1 && $NEED_PROFILE_RT -eq 1 && -f "$SCRIPT_DIR/lib/libclang_rt.profile.a" ]]; then PARSED_ARGS+=("$SCRIPT_DIR/lib/libclang_rt.profile.a" "-Wl,-u,__llvm_profile_runtime") fi diff --git a/src/SPC/util/PgoManager.php b/src/SPC/util/PgoManager.php index 854e687e..efb75c0d 100644 --- a/src/SPC/util/PgoManager.php +++ b/src/SPC/util/PgoManager.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace SPC\util; +use SPC\builder\BuilderBase; use SPC\exception\WrongUsageException; use SPC\store\FileSystem; use SPC\store\SourcePatcher; @@ -16,6 +17,8 @@ class PgoManager { public const MODE_INSTRUMENT = 'instrument'; + public const MODE_CS_INSTRUMENT = 'cs-instrument'; + public const MODE_USE = 'use'; private const TRAINABLE = [ @@ -68,6 +71,23 @@ class PgoManager logger()->info('pgo --pgi: instrumented build, profraw will land under ' . $this->profileRoot . '//'); } + /** Setup --cs-pgi: build with -fprofile-use= -fcs-profile-generate=. Requires existing .profdata. */ + public function setupCsInstrument(int $rule): void + { + $this->validateRule($rule); + foreach ($this->trainableIn($rule) as $sapi) { + if (!is_file($this->profDataFile($sapi))) { + throw new WrongUsageException("--cs-pgi: missing {$sapi}.profdata; run --pgi + --pgo first"); + } + f_mkdir($this->csRawDir($sapi), recursive: true); + } + $this->mode = self::MODE_CS_INSTRUMENT; + self::$active = $this; + $this->applyShutdownPatches(); + $this->applyForSapi($this->trainableIn($rule)[0]); + logger()->info('pgo --cs-pgi: cs-instrumented build, cs-profraw under ' . $this->profileRoot . '/cs-/'); + } + /** Setup --pgo: merge collected .profraw, then build with -fprofile-use=. */ public function setupUse(int $rule): void { @@ -83,6 +103,29 @@ class PgoManager $this->applyForSapi($this->trainableIn($rule)[0]); } + /** Patches php-src/libtool to passthrough -fcs-profile-* flags (otherwise dropped during shared lib link). */ + public static function patchBeforeMake(BuilderBase $builder): void + { + if (!$builder->getOption('cs-pgi')) { + return; + } + $libtool = SOURCE_PATH . '/php-src/libtool'; + if (!is_file($libtool)) { + return; + } + $contents = file_get_contents($libtool); + if (str_contains($contents, '-fcs-profile-*')) { + return; + } + $patched = str_replace('-fprofile-*|-F*', '-fprofile-*|-fcs-profile-*|-F*', $contents); + if ($patched === $contents) { + logger()->warning('pgo --cs-pgi: could not patch libtool for -fcs-profile-* passthrough'); + return; + } + file_put_contents($libtool, $patched); + logger()->info('pgo --cs-pgi: patched libtool for -fcs-profile-* passthrough'); + } + public function applyForSapi(string $sapi): void { $sapi = $this->resolveSapi($sapi); @@ -95,12 +138,18 @@ class PgoManager $this->setFlag('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS_PROGRAM', ''); return; } - $flags = $this->mode === self::MODE_INSTRUMENT - ? '-fprofile-generate=' . $this->rawDir($sapi) - : '-fprofile-use=' . $this->profDataFile($sapi) + $flags = match ($this->mode) { + self::MODE_INSTRUMENT => '-fprofile-generate=' . $this->rawDir($sapi), + self::MODE_CS_INSTRUMENT => '-fprofile-use=' . $this->profDataFile($sapi) + . ' -fcs-profile-generate=' . $this->csRawDir($sapi) . ' -Wno-error=profile-instr-unprofiled' . ' -Wno-error=profile-instr-out-of-date' - . ' -Wno-backend-plugin'; + . ' -Wno-backend-plugin', + default => '-fprofile-use=' . $this->profDataFile($sapi) + . ' -Wno-error=profile-instr-unprofiled' + . ' -Wno-error=profile-instr-out-of-date' + . ' -Wno-backend-plugin', + }; $this->setFlag('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS', $flags); $this->setFlag('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS_PROGRAM', $this->ldOnly($flags, $sapi)); logger()->info("pgo {$this->mode} ({$sapi})"); @@ -129,7 +178,8 @@ class PgoManager private function mergeSapi(string $sapi): void { $raws = glob($this->rawDir($sapi) . '/*.profraw') ?: []; - if (empty($raws)) { + $csRaws = glob($this->csRawDir($sapi) . '/*.profraw') ?: []; + if (empty($raws) && empty($csRaws)) { if ($sapi === 'frankenphp') { logger()->warning('pgo --pgo: no .profraw for frankenphp (cgo glue PGO will be skipped); run --pgi, exercise frankenphp longer, then re-run --pgo to include it'); return; @@ -137,7 +187,8 @@ class PgoManager throw new WrongUsageException("--pgo: no .profraw for {$sapi}; run --pgi, exercise the binary, then re-run --pgo"); } $out = $this->profDataFile($sapi); - $argv = implode(' ', array_map('escapeshellarg', $raws)); + $inputs = array_merge($raws, $csRaws); + $argv = implode(' ', array_map('escapeshellarg', $inputs)); shell()->exec('llvm-profdata merge --failure-mode=warn -output=' . escapeshellarg($out) . ' ' . $argv); if (!is_file($out) || filesize($out) === 0) { throw new WrongUsageException("--pgo: empty merge output for {$sapi}"); @@ -150,6 +201,11 @@ class PgoManager return $this->profileRoot . '/' . $sapi; } + private function csRawDir(string $sapi): string + { + return $this->profileRoot . '/cs-' . $sapi; + } + private function profDataFile(string $sapi): string { return $this->profileRoot . '/' . $sapi . '.profdata'; @@ -175,7 +231,7 @@ class PgoManager private function setFlag(string $var, string $append): void { $cur = (string) getenv($var); - $cur = preg_replace('/\s*-fprofile-(generate|use)=\S+/', '', $cur) ?? $cur; + $cur = preg_replace('/\s*-f(cs-)?profile-(generate|use)=\S+/', '', $cur) ?? $cur; $cur = preg_replace('/\s*-Wno-error=profile-instr-\S+/', '', $cur) ?? $cur; $cur = preg_replace('/\s*-Wno-backend-plugin/', '', $cur) ?? $cur; f_putenv($var . '=' . trim($cur . ' ' . $append)); @@ -191,6 +247,7 @@ class PgoManager $patterns = ['/\s*-Wno-error=\S+/', '/\s*-Wno-backend-plugin/']; if ($sapi === 'frankenphp') { $patterns[] = '/\s*-fprofile-use=\S+/'; + $patterns[] = '/\s*-fcs-profile-generate=\S+/'; } return trim(preg_replace($patterns, '', $flags) ?? $flags); } diff --git a/src/globals/patch/spc_pgo_flush_php_main.patch b/src/globals/patch/spc_pgo_flush_php_main.patch index 56bb2862..6bf905bf 100644 --- a/src/globals/patch/spc_pgo_flush_php_main.patch +++ b/src/globals/patch/spc_pgo_flush_php_main.patch @@ -1,13 +1,10 @@ --- a/main/main.c +++ b/main/main.c -@@ -2563,6 +2563,12 @@ +@@ -2563,6 +2563,9 @@ #endif zend_observer_shutdown(); + -+ /* spc-pgo: explicit profile flush so embed/frankenphp callers that exit -+ * via SYS_exit_group (skipping libc atexit) still get .profraw written. -+ * Weak symbol → no-op in non-PGO builds. */ + { extern int __llvm_profile_write_file(void) __attribute__((weak)); + if (__llvm_profile_write_file) __llvm_profile_write_file(); } }