mirror of
https://github.com/crazywhalecc/static-php-cli.git
synced 2026-07-02 14:25:41 +08:00
add --pgi and --pgo to facilitate PGO builds
This commit is contained in:
@@ -187,14 +187,6 @@ class Extension
|
||||
*/
|
||||
public function patchBeforeMake(): bool
|
||||
{
|
||||
if (SPCTarget::getTargetOS() === 'Linux' && $this->isBuildShared() && ($objs = getenv('SPC_EXTRA_RUNTIME_OBJECTS'))) {
|
||||
FileSystem::replaceFileRegex(
|
||||
SOURCE_PATH . '/php-src/Makefile',
|
||||
"/^(shared_objects_{$this->getName()}\\s*=.*)$/m",
|
||||
"$1 {$objs}",
|
||||
);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -244,13 +236,6 @@ class Extension
|
||||
);
|
||||
}
|
||||
|
||||
if ($objs = getenv('SPC_EXTRA_RUNTIME_OBJECTS')) {
|
||||
FileSystem::replaceFileRegex(
|
||||
$this->source_dir . '/Makefile',
|
||||
"/^(shared_objects_{$this->getName()}\\s*=.*)$/m",
|
||||
"$1 {$objs}",
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ use SPC\store\SourcePatcher;
|
||||
use SPC\toolchain\ToolchainManager;
|
||||
use SPC\toolchain\ZigToolchain;
|
||||
use SPC\util\GlobalEnvManager;
|
||||
use SPC\util\PgoManager;
|
||||
use SPC\util\SPCConfigUtil;
|
||||
use SPC\util\SPCTarget;
|
||||
|
||||
@@ -132,32 +133,36 @@ class LinuxBuilder extends UnixBuilderBase
|
||||
|
||||
$this->cleanMake();
|
||||
|
||||
if ($enableCli) {
|
||||
logger()->info('building cli');
|
||||
$this->buildCli();
|
||||
}
|
||||
if ($enableFpm) {
|
||||
logger()->info('building fpm');
|
||||
$this->buildFpm();
|
||||
}
|
||||
if ($enableCgi) {
|
||||
logger()->info('building cgi');
|
||||
$this->buildCgi();
|
||||
}
|
||||
if ($enableMicro) {
|
||||
logger()->info('building micro');
|
||||
$this->buildMicro();
|
||||
}
|
||||
if ($enableEmbed) {
|
||||
logger()->info('building embed');
|
||||
if ($enableMicro) {
|
||||
FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/Makefile', 'OVERALL_TARGET =', 'OVERALL_TARGET = libphp.la');
|
||||
$pgo = PgoManager::active();
|
||||
$needsClean = false;
|
||||
$sapiBuilds = [
|
||||
['cli', $enableCli, true, fn () => $this->buildCli()],
|
||||
['fpm', $enableFpm, true, fn () => $this->buildFpm()],
|
||||
['cgi', $enableCgi, true, fn () => $this->buildCgi()],
|
||||
['micro', $enableMicro, true, fn () => $this->buildMicro()],
|
||||
['embed', $enableEmbed, true, function () use ($enableMicro): void {
|
||||
if ($enableMicro) {
|
||||
FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/Makefile', 'OVERALL_TARGET =', 'OVERALL_TARGET = libphp.la');
|
||||
}
|
||||
$this->buildEmbed();
|
||||
}],
|
||||
// frankenphp doesn't rebuild php-src; xcaddy links against the deployed libphp.so
|
||||
['frankenphp', $enableFrankenphp, false, fn () => $this->buildFrankenphp()],
|
||||
];
|
||||
|
||||
foreach ($sapiBuilds as [$sapi, $enabled, $rebuildsPhpSrc, $build]) {
|
||||
if (!$enabled) {
|
||||
continue;
|
||||
}
|
||||
$this->buildEmbed();
|
||||
}
|
||||
if ($enableFrankenphp) {
|
||||
logger()->info('building frankenphp');
|
||||
$this->buildFrankenphp();
|
||||
if ($pgo) {
|
||||
if ($needsClean && $rebuildsPhpSrc) {
|
||||
$this->cleanMake();
|
||||
}
|
||||
$pgo->applyForSapi($sapi);
|
||||
$needsClean = $needsClean || $rebuildsPhpSrc;
|
||||
}
|
||||
logger()->info('building ' . $sapi);
|
||||
$build();
|
||||
}
|
||||
$shared_extensions = array_map('trim', array_filter(explode(',', $this->getOption('build-shared'))));
|
||||
if (!empty($shared_extensions)) {
|
||||
@@ -327,11 +332,18 @@ class LinuxBuilder extends UnixBuilderBase
|
||||
$config = (new SPCConfigUtil($this, ['libs_only_deps' => true, 'absolute_libs' => true]))->config($this->ext_list, $this->lib_list, $this->getOption('with-suggested-exts'), $this->getOption('with-suggested-libs'));
|
||||
$static = SPCTarget::isStatic() ? '-all-static' : '';
|
||||
$lib = BUILD_LIB_PATH;
|
||||
$extra_ldflags = (string) getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS');
|
||||
if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'shared'
|
||||
&& !str_contains($extra_ldflags, '-avoid-version')
|
||||
&& !preg_match('/-release\s+\S+/', $extra_ldflags)) {
|
||||
$extra_ldflags = trim($extra_ldflags . ' -avoid-version -module');
|
||||
}
|
||||
$extra_ldflags_program = trim("-L{$lib} {$static} -pie " . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS_PROGRAM'));
|
||||
return array_filter([
|
||||
'EXTRA_CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'),
|
||||
'EXTRA_LIBS' => $config['libs'],
|
||||
'EXTRA_LDFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'),
|
||||
'EXTRA_LDFLAGS_PROGRAM' => "-L{$lib} {$static} -pie",
|
||||
'EXTRA_LDFLAGS' => $extra_ldflags,
|
||||
'EXTRA_LDFLAGS_PROGRAM' => $extra_ldflags_program,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -450,10 +450,11 @@ abstract class UnixBuilderBase extends BuilderBase
|
||||
$cflags .= ' -Wno-error=missing-profile';
|
||||
$libs .= ' -lgcov';
|
||||
}
|
||||
$extraLdProgram = (string) getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS_PROGRAM');
|
||||
$env = [...[
|
||||
'CGO_ENABLED' => '1',
|
||||
'CGO_CFLAGS' => clean_spaces($cflags),
|
||||
'CGO_LDFLAGS' => "{$this->arch_ld_flags} {$staticFlags} {$config['ldflags']} {$libs}",
|
||||
'CGO_LDFLAGS' => trim("{$this->arch_ld_flags} {$staticFlags} {$config['ldflags']} {$libs} {$extraLdProgram}"),
|
||||
'XCADDY_GO_BUILD_FLAGS' => '-buildmode=pie ' .
|
||||
'-ldflags \"-linkmode=external ' . $extLdFlags . ' ' .
|
||||
'-X \'github.com/caddyserver/caddy/v2/modules/caddyhttp.ServerHeader=FrankenPHP Caddy\' ' .
|
||||
|
||||
@@ -12,6 +12,7 @@ use SPC\store\SourcePatcher;
|
||||
use SPC\util\DependencyUtil;
|
||||
use SPC\util\GlobalEnvManager;
|
||||
use SPC\util\LicenseDumper;
|
||||
use SPC\util\PgoManager;
|
||||
use SPC\util\SPCTarget;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
@@ -49,6 +50,8 @@ class BuildPHPCommand extends BuildCommand
|
||||
$this->addOption('with-micro-logo', null, InputOption::VALUE_REQUIRED, 'Use custom .ico for micro.sfx (windows only)');
|
||||
$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('pgo', null, null, 'Build optimised binaries (-fprofile-use) from .profraw collected by a previous --pgi run.');
|
||||
}
|
||||
|
||||
public function handle(): int
|
||||
@@ -210,9 +213,19 @@ class BuildPHPCommand extends BuildCommand
|
||||
|
||||
// clean old modules that may conflict with the new php build
|
||||
FileSystem::removeDir(BUILD_MODULES_PATH);
|
||||
// start to build
|
||||
$builder->buildPHP($rule);
|
||||
|
||||
$pgi = (bool) $this->getOption('pgi');
|
||||
$pgo = (bool) $this->getOption('pgo');
|
||||
if ($pgi && $pgo) {
|
||||
$this->output->writeln('<error>--pgi and --pgo are mutually exclusive</error>');
|
||||
return static::FAILURE;
|
||||
}
|
||||
if ($pgi) {
|
||||
(new PgoManager())->setupInstrument($rule);
|
||||
} elseif ($pgo) {
|
||||
(new PgoManager())->setupUse($rule);
|
||||
}
|
||||
$builder->buildPHP($rule);
|
||||
$builder->testPHP($rule);
|
||||
|
||||
// compile stopwatch :P
|
||||
|
||||
@@ -116,18 +116,17 @@ class Zig extends CustomPackage
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($all_exist) {
|
||||
return;
|
||||
if (!$all_exist) {
|
||||
$lock = json_decode(FileSystem::readFile(LockFile::LOCK_FILE), true);
|
||||
$source_type = $lock[$name]['source_type'];
|
||||
$filename = DOWNLOAD_PATH . '/' . ($lock[$name]['filename'] ?? $lock[$name]['dirname']);
|
||||
$extract = "{$pkgroot}/zig";
|
||||
|
||||
FileSystem::extractPackage($name, $source_type, $filename, $extract);
|
||||
|
||||
$this->createZigCcScript($zig_bin_dir);
|
||||
}
|
||||
|
||||
$lock = json_decode(FileSystem::readFile(LockFile::LOCK_FILE), true);
|
||||
$source_type = $lock[$name]['source_type'];
|
||||
$filename = DOWNLOAD_PATH . '/' . ($lock[$name]['filename'] ?? $lock[$name]['dirname']);
|
||||
$extract = "{$pkgroot}/zig";
|
||||
|
||||
FileSystem::extractPackage($name, $source_type, $filename, $extract);
|
||||
|
||||
$this->createZigCcScript($zig_bin_dir);
|
||||
$this->buildClangRuntimeBits($zig_bin_dir);
|
||||
}
|
||||
|
||||
public static function getEnvironment(): array
|
||||
@@ -140,6 +139,145 @@ class Zig extends CustomPackage
|
||||
return PKG_ROOT_PATH . '/zig';
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the bits of clang's runtime that zig 0.16 doesn't ship: the
|
||||
* profile runtime (so -fprofile-generate actually emits .profraw) and
|
||||
* crtbegin.o/crtend.o (so shared libraries get __dso_handle and the
|
||||
* __cxa_finalize atexit hook).
|
||||
*
|
||||
* Build from 2mb compiler-rt-<llvm>.src tar
|
||||
* to avoid downloading 2gb full prebuilt tarball.
|
||||
*/
|
||||
private function buildClangRuntimeBits(string $zig_bin_dir): void
|
||||
{
|
||||
if (PHP_OS_FAMILY !== 'Linux') {
|
||||
return;
|
||||
}
|
||||
$libDir = "{$zig_bin_dir}/lib";
|
||||
$profileLib = "{$libDir}/libclang_rt.profile.a";
|
||||
$crtBegin = "{$libDir}/clang_rt.crtbegin.o";
|
||||
$crtEnd = "{$libDir}/clang_rt.crtend.o";
|
||||
if (file_exists($profileLib) && file_exists($crtBegin) && file_exists($crtEnd)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$zig = "{$zig_bin_dir}/zig";
|
||||
$verLine = trim((string)shell_exec(escapeshellarg($zig) . ' cc --version 2>/dev/null'));
|
||||
if (!preg_match('/clang version (\d+\.\d+\.\d+)/', $verLine, $m)) {
|
||||
logger()->warning('[zig] could not detect bundled clang version; skipping runtime bit build (--pgo + shared libs without __dso_handle)');
|
||||
return;
|
||||
}
|
||||
$llvmVersion = $m[1];
|
||||
logger()->info("Building clang runtime bits for LLVM {$llvmVersion} (zig's bundled clang)");
|
||||
|
||||
$srcRoot = $this->fetchCompilerRtSource($llvmVersion);
|
||||
if ($srcRoot === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
f_mkdir($libDir, recursive: true);
|
||||
if (!file_exists($profileLib)) {
|
||||
$this->buildProfileRuntime($zig, $srcRoot, $profileLib);
|
||||
}
|
||||
if (!file_exists($crtBegin) || !file_exists($crtEnd)) {
|
||||
$this->buildCrtObjects($zig, $srcRoot, $crtBegin, $crtEnd);
|
||||
}
|
||||
FileSystem::removeDir($srcRoot);
|
||||
}
|
||||
|
||||
private function fetchCompilerRtSource(string $llvmVersion): ?string
|
||||
{
|
||||
$pkgName = "compiler-rt-{$llvmVersion}";
|
||||
$tarball = "compiler-rt-{$llvmVersion}.src.tar.xz";
|
||||
$url = "https://github.com/llvm/llvm-project/releases/download/llvmorg-{$llvmVersion}/{$tarball}";
|
||||
try {
|
||||
Downloader::downloadPackage($pkgName, [
|
||||
'type' => 'url',
|
||||
'url' => $url,
|
||||
'filename' => $tarball,
|
||||
]);
|
||||
}
|
||||
catch (\Throwable $e) {
|
||||
logger()->warning("[zig] failed to download {$tarball}: {$e->getMessage()}");
|
||||
return null;
|
||||
}
|
||||
$srcRoot = PKG_ROOT_PATH . "/compiler-rt-src-{$llvmVersion}";
|
||||
FileSystem::removeDir($srcRoot);
|
||||
FileSystem::extractPackage($pkgName, SPC_SOURCE_ARCHIVE, DOWNLOAD_PATH . '/' . $tarball, $srcRoot);
|
||||
return $srcRoot;
|
||||
}
|
||||
|
||||
private function buildProfileRuntime(string $zig, string $srcRoot, string $libPath): void
|
||||
{
|
||||
$profileSrc = "{$srcRoot}/lib/profile";
|
||||
$profileInc = "{$srcRoot}/include";
|
||||
if (!is_dir($profileSrc)) {
|
||||
logger()->warning("[zig] profile src dir missing at {$profileSrc} — --pgo will not work");
|
||||
return;
|
||||
}
|
||||
$sources = array_merge(
|
||||
glob("{$profileSrc}/*.c") ?: [],
|
||||
glob("{$profileSrc}/*.cpp") ?: []
|
||||
);
|
||||
$skip = ['/PlatformAIX', '/PlatformDarwin', '/PlatformFuchsia', '/PlatformOther', '/PlatformWindows', '/WindowsMMap'];
|
||||
$sources = array_filter($sources, function ($f) use ($skip) {
|
||||
foreach ($skip as $s) {
|
||||
if (str_contains($f, $s)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
$objDir = "{$srcRoot}/obj-profile";
|
||||
f_mkdir($objDir, recursive: true);
|
||||
$cflags = '-c -O2 -fPIC -fvisibility=hidden ' .
|
||||
'-I' . escapeshellarg($profileInc) . ' ' .
|
||||
'-DCOMPILER_RT_HAS_ATOMICS=1 -DCOMPILER_RT_HAS_FCNTL_LCK=1 -DCOMPILER_RT_HAS_UNAME=1';
|
||||
$objs = [];
|
||||
foreach ($sources as $src) {
|
||||
$obj = $objDir . '/' . pathinfo($src, PATHINFO_FILENAME) . '.o';
|
||||
$cmd = escapeshellarg($zig) . ' cc ' . $cflags . ' -o ' . escapeshellarg($obj) . ' ' . escapeshellarg($src) . ' 2>&1';
|
||||
if (!$this->runZigCmd($cmd, $obj, "failed to compile {$src}")) {
|
||||
return;
|
||||
}
|
||||
$objs[] = $obj;
|
||||
}
|
||||
$arCmd = escapeshellarg($zig) . ' ar rcs ' . escapeshellarg($libPath) . ' ' . implode(' ', array_map('escapeshellarg', $objs)) . ' 2>&1';
|
||||
if (!$this->runZigCmd($arCmd, $libPath, 'zig ar failed')) {
|
||||
return;
|
||||
}
|
||||
logger()->info('[zig] libclang_rt.profile.a installed (' . filesize($libPath) . ' bytes)');
|
||||
}
|
||||
|
||||
private function buildCrtObjects(string $zig, string $srcRoot, string $crtBegin, string $crtEnd): void
|
||||
{
|
||||
$beginSrc = "{$srcRoot}/lib/builtins/crtbegin.c";
|
||||
$endSrc = "{$srcRoot}/lib/builtins/crtend.c";
|
||||
if (!is_file($beginSrc) || !is_file($endSrc)) {
|
||||
logger()->error("[zig] crtbegin/crtend source missing under {$srcRoot}/lib/builtins — shared libs will lack __dso_handle");
|
||||
return;
|
||||
}
|
||||
$cflags = '-c -O2 -fPIC -fvisibility=hidden -DCRT_HAS_INITFINI_ARRAY';
|
||||
foreach ([[$beginSrc, $crtBegin], [$endSrc, $crtEnd]] as [$src, $dst]) {
|
||||
$cmd = escapeshellarg($zig) . ' cc ' . $cflags . ' -o ' . escapeshellarg($dst) . ' ' . escapeshellarg($src) . ' 2>&1';
|
||||
if (!$this->runZigCmd($cmd, $dst, "failed to compile {$src}")) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
logger()->info('[zig] clang_rt.crtbegin.o + clang_rt.crtend.o installed (' . filesize($crtBegin) . ' + ' . filesize($crtEnd) . ' bytes)');
|
||||
}
|
||||
|
||||
private function runZigCmd(string $cmd, string $dst, string $errPrefix): bool
|
||||
{
|
||||
exec($cmd, $out, $rc);
|
||||
if ($rc !== 0 || !is_file($dst)) {
|
||||
logger()->warning("[zig] {$errPrefix}: " . implode("\n", $out));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private function createZigCcScript(string $bin_dir): void
|
||||
{
|
||||
$script_path = __DIR__ . '/../scripts/zig-cc.sh';
|
||||
|
||||
@@ -39,6 +39,24 @@ while [[ $# -gt 0 ]]; do
|
||||
esac
|
||||
done
|
||||
|
||||
IS_LINK=1
|
||||
NEED_PROFILE_RT=0 # https://codeberg.org/ziglang/zig/issues/32066
|
||||
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 ;;
|
||||
-shared) NEED_CRT=1 ;;
|
||||
esac
|
||||
done
|
||||
[[ "$SPC_COMPILER_EXTRA" == *-fprofile-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
|
||||
if [[ $IS_LINK -eq 1 && $NEED_CRT -eq 1 && -f "$SCRIPT_DIR/lib/clang_rt.crtbegin.o" && -f "$SCRIPT_DIR/lib/clang_rt.crtend.o" ]]; then
|
||||
PARSED_ARGS+=("$SCRIPT_DIR/lib/clang_rt.crtbegin.o" "$SCRIPT_DIR/lib/clang_rt.crtend.o")
|
||||
fi
|
||||
|
||||
[[ -n "$SPC_TARGET" ]] && TARGET="-target $SPC_TARGET" || TARGET=""
|
||||
|
||||
if [[ "$SPC_TARGET" =~ \.[0-9]+\.[0-9]+ ]]; then
|
||||
|
||||
@@ -17,27 +17,6 @@ class ZigToolchain implements ToolchainInterface
|
||||
GlobalEnvManager::putenv('SPC_LINUX_DEFAULT_CXX=zig-c++');
|
||||
GlobalEnvManager::putenv('SPC_LINUX_DEFAULT_AR=zig-ar');
|
||||
GlobalEnvManager::putenv('SPC_LINUX_DEFAULT_LD=zig-ld.lld');
|
||||
|
||||
// Generate additional objects needed for zig toolchain
|
||||
$paths = ['/usr/lib/gcc', '/usr/local/lib/gcc'];
|
||||
$objects = ['crtbeginS.o', 'crtendS.o'];
|
||||
$found = [];
|
||||
|
||||
foreach ($objects as $obj) {
|
||||
$located = null;
|
||||
foreach ($paths as $base) {
|
||||
$output = shell_exec("find {$base} -name {$obj} 2>/dev/null | grep -v '/32/' | head -n 1");
|
||||
$line = trim((string) $output);
|
||||
if ($line !== '') {
|
||||
$located = $line;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($located) {
|
||||
$found[] = $located;
|
||||
}
|
||||
}
|
||||
GlobalEnvManager::putenv('SPC_EXTRA_RUNTIME_OBJECTS=' . implode(' ', $found));
|
||||
}
|
||||
|
||||
public function afterInit(): void
|
||||
|
||||
179
src/SPC/util/PgoManager.php
Normal file
179
src/SPC/util/PgoManager.php
Normal file
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SPC\util;
|
||||
|
||||
use SPC\exception\WrongUsageException;
|
||||
use SPC\store\FileSystem;
|
||||
|
||||
/**
|
||||
* Two-call PGO driver: --pgi instruments, --pgo uses the .profraw the user
|
||||
* collected by running the instrumented binaries. PgoManager only sets the
|
||||
* compiler flags; it does not run any workload itself.
|
||||
*/
|
||||
class PgoManager
|
||||
{
|
||||
/**
|
||||
* SAPIs whose clang-compiled output can be PGO'd. frankenphp is included
|
||||
* because its cgo glue is C compiled by zig — the Go side it wraps is
|
||||
* not clang-PGO'd here. libphp.so is the embed SAPI; running frankenphp
|
||||
* produces profile data for embed (because it loads libphp.so) AND for
|
||||
* frankenphp (because the cgo glue runs too).
|
||||
*/
|
||||
private const TRAINABLE = [
|
||||
'cli' => BUILD_TARGET_CLI,
|
||||
'micro' => BUILD_TARGET_MICRO,
|
||||
'cgi' => BUILD_TARGET_CGI,
|
||||
'fpm' => BUILD_TARGET_FPM,
|
||||
'embed' => BUILD_TARGET_EMBED,
|
||||
'frankenphp' => BUILD_TARGET_FRANKENPHP,
|
||||
];
|
||||
|
||||
public const MODE_INSTRUMENT = 'instrument';
|
||||
|
||||
public const MODE_USE = 'use';
|
||||
|
||||
private string $profileRoot;
|
||||
|
||||
private string $mode;
|
||||
|
||||
private static ?self $active = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->profileRoot = BUILD_ROOT_PATH . '/pgo-data';
|
||||
}
|
||||
|
||||
public static function active(): ?self
|
||||
{
|
||||
return self::$active;
|
||||
}
|
||||
|
||||
/** Setup --pgi: build with -fprofile-generate=<sapi-dir>. */
|
||||
public function setupInstrument(int $rule): void
|
||||
{
|
||||
$this->validateRule($rule);
|
||||
FileSystem::removeDir($this->profileRoot);
|
||||
f_mkdir($this->profileRoot, recursive: true);
|
||||
foreach ($this->trainableIn($rule) as $sapi) {
|
||||
f_mkdir($this->rawDir($sapi), recursive: true);
|
||||
}
|
||||
$this->mode = self::MODE_INSTRUMENT;
|
||||
self::$active = $this;
|
||||
$this->applyForSapi($this->trainableIn($rule)[0]);
|
||||
logger()->info('pgo --pgi: instrumented build, profraw will land under ' . $this->profileRoot . '/<sapi>/');
|
||||
}
|
||||
|
||||
/** Setup --pgo: merge collected .profraw, then build with -fprofile-use=<sapi.profdata>. */
|
||||
public function setupUse(int $rule): void
|
||||
{
|
||||
$this->validateRule($rule);
|
||||
if (trim((string) shell_exec('command -v llvm-profdata 2>/dev/null')) === '') {
|
||||
throw new WrongUsageException('--pgo: llvm-profdata not on PATH');
|
||||
}
|
||||
foreach ($this->trainableIn($rule) as $sapi) {
|
||||
$this->mergeSapi($sapi);
|
||||
}
|
||||
$this->mode = self::MODE_USE;
|
||||
self::$active = $this;
|
||||
$this->applyForSapi($this->trainableIn($rule)[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set EXTRA_CFLAGS / EXTRA_LDFLAGS_PROGRAM for the SAPI about to be built.
|
||||
* Non-trainable SAPIs (e.g. frankenphp's Go side) are left untouched.
|
||||
*/
|
||||
public function applyForSapi(string $sapi): void
|
||||
{
|
||||
$sapi = $this->resolveSapi($sapi);
|
||||
if (!isset(self::TRAINABLE[$sapi])) {
|
||||
return;
|
||||
}
|
||||
$flags = $this->mode === self::MODE_INSTRUMENT
|
||||
? '-fprofile-generate=' . $this->rawDir($sapi) . ' -fprofile-continuous -mllvm -disable-vp'
|
||||
: '-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));
|
||||
logger()->info("pgo {$this->mode} ({$sapi})");
|
||||
}
|
||||
|
||||
/**
|
||||
* In static-embed mode libphp.a is linked into frankenphp, and the linker
|
||||
* resolves all `__llvm_profile_filename` references to a single path —
|
||||
* the embed SAPI's per-TU `-fprofile-generate=…` setting is silently
|
||||
* dropped. Compile libphp.a with frankenphp's path so all counter writes
|
||||
* agree on one file, and read libphp.a's PGO from frankenphp.profdata.
|
||||
*/
|
||||
private function resolveSapi(string $sapi): string
|
||||
{
|
||||
if ($sapi === 'embed' && getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'static') {
|
||||
return 'frankenphp';
|
||||
}
|
||||
return $sapi;
|
||||
}
|
||||
|
||||
private function validateRule(int $rule): void
|
||||
{
|
||||
if (empty($this->trainableIn($rule))) {
|
||||
throw new WrongUsageException('--pgi/--pgo: no trainable SAPI in build rule (need one of: ' . implode(', ', array_keys(self::TRAINABLE)) . ')');
|
||||
}
|
||||
}
|
||||
|
||||
private function mergeSapi(string $sapi): void
|
||||
{
|
||||
$raws = glob($this->rawDir($sapi) . '/*.profraw') ?: [];
|
||||
if (empty($raws)) {
|
||||
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));
|
||||
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}");
|
||||
}
|
||||
logger()->info("pgo merged {$sapi}: " . filesize($out) . ' bytes');
|
||||
}
|
||||
|
||||
private function rawDir(string $sapi): string
|
||||
{
|
||||
return $this->profileRoot . '/' . $sapi;
|
||||
}
|
||||
|
||||
private function profDataFile(string $sapi): string
|
||||
{
|
||||
return $this->profileRoot . '/' . $sapi . '.profdata';
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
private function trainableIn(int $rule): array
|
||||
{
|
||||
$out = [];
|
||||
foreach (self::TRAINABLE as $sapi => $mask) {
|
||||
if (($rule & $mask) !== $mask) {
|
||||
continue;
|
||||
}
|
||||
$resolved = $this->resolveSapi($sapi);
|
||||
if (!in_array($resolved, $out, true)) {
|
||||
$out[] = $resolved;
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/** Strip the previous PGO flags from $var and append the new ones. */
|
||||
private function setFlag(string $var, string $append): void
|
||||
{
|
||||
$cur = (string) getenv($var);
|
||||
$cur = preg_replace('/\s*-fprofile-(generate|use)=\S+/', '', $cur) ?? $cur;
|
||||
$cur = str_replace([' -fprofile-continuous', ' -mllvm -disable-vp'], '', $cur);
|
||||
$cur = preg_replace('/\s*-Wno-error=profile-instr-unprofiled\s+-Wno-error=profile-instr-out-of-date\s+-Wno-backend-plugin/', '', $cur) ?? $cur;
|
||||
f_putenv($var . '=' . trim($cur . ' ' . $append));
|
||||
}
|
||||
|
||||
/** Linker only takes -fprofile-{generate,use}; strip the codegen-only -mllvm and warning flags. */
|
||||
private function ldOnly(string $flags): string
|
||||
{
|
||||
return preg_replace(['/\s*-mllvm\s+\S+/', '/\s*-Wno-error=\S+/', '/\s*-Wno-backend-plugin/'], '', $flags) ?? $flags;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user