diff --git a/examples/README.md b/examples/README.md index d36bf4325..83294681a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -176,16 +176,16 @@ For **001-SimpleWeb**, `bin/compile.php` is timed **without** compile-time `-q`; For **003-MiniWebApp**, VM/JIT/native columns run `public/index.php` with `PATH_INFO=/home` (and related CGI env) from the example `public/` directory ([#491](https://github.com/PurHur/php-compiler/issues/491), runtime [#539](https://github.com/PurHur/php-compiler/issues/539)). AOT columns time `phpc build --project` and `.phpc/bin/app` with the same CGI overlay when LLVM is ready and execute returns HTML ([#716](https://github.com/PurHur/php-compiler/issues/716); execute [#764](https://github.com/PurHur/php-compiler/issues/764) closed). The row is omitted when `phpc lint --all examples/003-MiniWebApp` fails unless `BENCH_MINIWEBAPP=1`. -For **005-SessionsWeb**, the benchmark row is omitted until `phpc lint --all examples/005-SessionsWeb` passes unless `BENCH_SESSIONSWEB=1` ([#1889](https://github.com/PurHur/php-compiler/issues/1889)). AOT columns stay `n/a` until two-request session execute is green ([#1891](https://github.com/PurHur/php-compiler/issues/1891)). +For **005-SessionsWeb**, the benchmark row is omitted until `phpc lint --all examples/005-SessionsWeb` passes unless `BENCH_SESSIONSWEB=1` ([#1889](https://github.com/PurHur/php-compiler/issues/1889)). AOT columns time `phpc build --project` and a two-request session flash on `.phpc/bin/app` when LLVM is ready ([#1891](https://github.com/PurHur/php-compiler/issues/1891), [#1973](https://github.com/PurHur/php-compiler/issues/1973)); use `BENCH_SESSIONSWEB_AOT=1 ./script/rebuild-examples.php` to force AOT columns on harness regen. | Example Name | Native PHP | bin/vm.php | bin/jit.php | bin/compile.php | ./compiled | |----------------------|-----------------|-----------------|-----------------|-----------------|-----------------| -| 000-HelloWorld | 0.00760 | 0.04387 | 0.17749 | 1.33163 | 0.00109 | -| 001-SimpleWeb | 0.00764 | 0.04682 | 0.18321 | 1.34592 | 0.00109 | -| 002-StaticWeb | 0.00770 | 0.04771 | 0.17846 | 1.35182 | 0.00111 | -| 003-MiniWebApp | 0.00838 | 0.08591 | 0.45166 | 1.75921 | 0.00098 | -| 004-ApiJson | 0.00837 | 0.04503 | 0.17964 | 1.36437 | 0.00128 | -| 005-SessionsWeb | 0.00855 | 0.04840 | 0.19095 | n/a | n/a | +| 000-HelloWorld | 0.00792 | 0.04623 | 0.20762 | 1.87538 | 0.00156 | +| 001-SimpleWeb | 0.00895 | 0.04740 | 0.18640 | 1.35069 | 0.00118 | +| 002-StaticWeb | 0.00980 | 0.05066 | 0.18906 | 1.36219 | 0.00123 | +| 003-MiniWebApp | 0.00771 | 0.09690 | 0.52931 | 1.83640 | 0.00106 | +| 004-ApiJson | 0.00850 | 0.04705 | 0.19092 | 2.98758 | 0.00116 | +| 005-SessionsWeb | 0.01742 | 0.05082 | 0.19587 | 1.43149 | 0.00277 | diff --git a/script/check-rebuild-examples-005-row.php b/script/check-rebuild-examples-005-row.php index c25a64455..2559611f7 100755 --- a/script/check-rebuild-examples-005-row.php +++ b/script/check-rebuild-examples-005-row.php @@ -9,11 +9,15 @@ * Benchmark row policy matches script/rebuild-examples.php (issue #1889): * - Include when BENCH_SESSIONSWEB=1 or phpc lint --all examples/005-SessionsWeb passes * - Omit when lint fails unless BENCH_SESSIONSWEB=1 + * - AOT columns: n/a when LLVM/execute probe fails; real timings when probe passes (#1891, #1973) * * Usage: * php script/check-rebuild-examples-005-row.php */ +require_once __DIR__.'/../test/support/CgiCookieJar.php'; +require_once __DIR__.'/../test/support/SessionsWebCgiEnv.php'; + $root = dirname(__DIR__); $readme = $root.'/examples/README.md'; $exampleDir = $root.'/examples/005-SessionsWeb'; @@ -49,8 +53,8 @@ if ($hasRow) { $rowLine = extract_sessions_web_benchmark_line($body); - if (null !== $rowLine && !benchmark_row_aot_columns_honest($rowLine)) { - $errors[] = 'examples/README.md: 005-SessionsWeb benchmark row must keep bin/compile.php and ./compiled as n/a until AOT execute is green (#1891)'; + if (null !== $rowLine && !benchmark_row_aot_columns_honest($rowLine, $root)) { + $errors[] = 'examples/README.md: 005-SessionsWeb benchmark AOT columns out of sync (run: BENCH_SESSIONSWEB=1 BENCH_SESSIONSWEB_AOT=1 ./script/rebuild-examples.php; #1973)'; } } @@ -127,12 +131,8 @@ function extract_sessions_web_benchmark_line(string $readmeBody): ?string return trim($line[0]); } -function benchmark_row_aot_columns_honest(string $rowLine): bool +function benchmark_row_aot_columns_honest(string $rowLine, string $repoRoot): bool { - if ('1' === getenv('BENCH_SESSIONSWEB_AOT')) { - return true; - } - $parts = array_map('trim', explode('|', $rowLine)); $parts = array_values(array_filter($parts, static fn (string $p): bool => '' !== $p)); if (count($parts) < 6) { @@ -140,6 +140,186 @@ function benchmark_row_aot_columns_honest(string $rowLine): bool } $compileCol = $parts[4] ?? ''; $compiledCol = $parts[5] ?? ''; + $compileNa = (bool) preg_match('/n\/a/i', $compileCol); + $compiledNa = (bool) preg_match('/n\/a/i', $compiledCol); + + if (!llvm_ready_for_check($repoRoot)) { + return true; + } + + if ('1' === getenv('BENCH_SESSIONSWEB_AOT')) { + return !$compileNa && !$compiledNa; + } + + if (sessions_web_aot_execute_probe($repoRoot)) { + return !$compileNa && !$compiledNa; + } + + return $compileNa && $compiledNa; +} + +function llvm_ready_for_check(string $repoRoot): bool +{ + $candidates = []; + $fromEnv = getenv('PHP_COMPILER_LLVM_PATH'); + if (false !== $fromEnv && '' !== $fromEnv) { + $candidates[] = $fromEnv; + } + $candidates[] = $repoRoot.'/.llvm'; + $candidates[] = '/opt/llvm9'; + foreach ($candidates as $dir) { + if (is_file($dir.'/libLLVM-9.so.1')) { + return true; + } + } + + return false; +} + +function sessions_web_aot_execute_probe(string $repoRoot): bool +{ + if ('0' === getenv('SESSIONS_WEB_AOT_PROBE')) { + return false; + } + if (!llvm_ready_for_check($repoRoot)) { + return false; + } + $phpc = $repoRoot.'/phpc'; + $project = $repoRoot.'/examples/005-SessionsWeb'; + $binary = $project.'/.phpc/bin/app'; + if (!is_executable($phpc) || !is_file($project.'/example.php')) { + return false; + } + + $sessionDir = sys_get_temp_dir().'/phpc_check_sessionsweb_'.uniqid('', true); + if (!@mkdir($sessionDir, 0700, true) && !is_dir($sessionDir)) { + return false; + } + + $env = []; + foreach ($_ENV as $key => $value) { + if (is_string($value)) { + $env[$key] = $value; + } + } + $llvmDir = null; + foreach ([getenv('PHP_COMPILER_LLVM_PATH') ?: '', $repoRoot.'/.llvm', '/opt/llvm9'] as $dir) { + if ('' !== $dir && is_file($dir.'/libLLVM-9.so.1')) { + $llvmDir = realpath($dir) ?: $dir; + break; + } + } + if (null !== $llvmDir) { + $env['PHP_COMPILER_LLVM_PATH'] = $llvmDir; + $ld = $env['LD_LIBRARY_PATH'] ?? ''; + $env['LD_LIBRARY_PATH'] = '' === $ld ? $llvmDir : $llvmDir.':'.$ld; + } + $env['PHP_COMPILER_SESSION_DIR'] = $sessionDir; + + if (!is_executable($binary)) { + $build = proc_open( + [$phpc, 'build', '--project', $project], + [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']], + $pipes, + $repoRoot, + $env + ); + if (!is_resource($build)) { + check_sessions_web_cleanup($sessionDir); + + return false; + } + fclose($pipes[0]); + stream_get_contents($pipes[1]); + $stderr = stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + if (0 !== proc_close($build)) { + check_sessions_web_cleanup($sessionDir); + + return false; + } + } + + if (!is_executable($binary)) { + check_sessions_web_cleanup($sessionDir); - return preg_match('/n\/a/i', $compileCol) && preg_match('/n\/a/i', $compiledCol); + return false; + } + + $ok = check_sessions_web_flash_probe($repoRoot, $binary, $env); + check_sessions_web_cleanup($sessionDir); + + return $ok; +} + +/** + * @param array $baseEnv + */ +function check_sessions_web_flash_probe(string $repoRoot, string $binary, array $baseEnv): bool +{ + $jar = new PHPCompiler\CgiCookieJar(); + $empty = check_run_binary($repoRoot, $binary, array_merge($baseEnv, PHPCompiler\SessionsWebCgiEnv::getEmpty())); + if (null === $empty) { + return false; + } + $jar->absorbFromCgiOutput($empty); + if (!$jar->hasCookie('PHPSESSID')) { + return false; + } + $cookie = $jar->httpCookieHeader(); + if (null === check_run_binary( + $repoRoot, + $binary, + array_merge($baseEnv, PHPCompiler\SessionsWebCgiEnv::postFlash('Saved'), ['HTTP_COOKIE' => $cookie]) + )) { + return false; + } + $flash = check_run_binary( + $repoRoot, + $binary, + array_merge($baseEnv, PHPCompiler\SessionsWebCgiEnv::getEmpty(), ['HTTP_COOKIE' => $jar->httpCookieHeader()]) + ); + if (null === $flash) { + return false; + } + + return str_contains($flash, 'Flash: Saved'); +} + +/** + * @param array $env + */ +function check_run_binary(string $repoRoot, string $binary, array $env): ?string +{ + $proc = proc_open( + [$binary], + [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']], + $pipes, + $repoRoot, + $env + ); + if (!is_resource($proc)) { + return null; + } + fclose($pipes[0]); + $stdout = stream_get_contents($pipes[1]); + fclose($pipes[1]); + fclose($pipes[2]); + if (0 !== proc_close($proc)) { + return null; + } + + return false !== $stdout ? $stdout : ''; +} + +function check_sessions_web_cleanup(string $sessionDir): void +{ + if (!is_dir($sessionDir)) { + return; + } + foreach (glob($sessionDir.'/sess_*') ?: [] as $file) { + @unlink($file); + } + @rmdir($sessionDir); } diff --git a/script/rebuild-examples.php b/script/rebuild-examples.php index f80f23a06..c2db46a61 100644 --- a/script/rebuild-examples.php +++ b/script/rebuild-examples.php @@ -10,6 +10,8 @@ */ require_once __DIR__.'/../vendor/autoload.php'; +require_once __DIR__.'/../test/support/CgiCookieJar.php'; +require_once __DIR__.'/../test/support/SessionsWebCgiEnv.php'; echo "Rebuilding Examples\n"; @@ -194,7 +196,8 @@ function exampleDisplayName(string $example): string * aot_compile_time_query: bool, * aot_run_env: array, * skip_aot: bool, - * project_aot: bool + * project_aot: bool, + * sessions_web_project_aot: bool * } */ function exampleProfile(string $example): array @@ -212,18 +215,19 @@ function exampleProfile(string $example): array 'aot_run_env' => [], 'skip_aot' => false, 'project_aot' => true, + 'sessions_web_project_aot' => false, ]; } if ('005-SessionsWeb' === exampleDisplayName($example)) { - // AOT columns stay n/a until two-request session execute is green (#1891). return [ 'query' => null, 'cgi_env' => [], 'aot_compile_time_query' => true, 'aot_run_env' => [], - 'skip_aot' => true, + 'skip_aot' => false, 'project_aot' => false, + 'sessions_web_project_aot' => true, ]; } @@ -240,6 +244,7 @@ function exampleProfile(string $example): array ], 'skip_aot' => false, 'project_aot' => false, + 'sessions_web_project_aot' => false, ]; } @@ -250,6 +255,7 @@ function exampleProfile(string $example): array 'aot_run_env' => [], 'skip_aot' => false, 'project_aot' => false, + 'sessions_web_project_aot' => false, ]; } @@ -479,6 +485,139 @@ function tryBenchmarkMiniWebAppProjectAot(string $repoRoot, array $benchEnv, arr return ['compile' => $compileTime, 'compiled' => $compiledTime]; } +/** + * phpc build --project + two-request session flash for 005-SessionsWeb (#1891, #1973). + * + * @param array $benchEnv + * + * @return array{compile: float, compiled: float}|null + */ +function tryBenchmarkSessionsWebProjectAot(string $repoRoot, array $benchEnv): ?array +{ + if ('0' === getenv('BENCH_SESSIONSWEB_AOT')) { + echo " 005-SessionsWeb AOT: skip (BENCH_SESSIONSWEB_AOT=0)\n"; + + return null; + } + + $project = $repoRoot.'/examples/005-SessionsWeb'; + $phpc = $repoRoot.'/phpc'; + $binary = $project.'/.phpc/bin/app'; + + if (!is_executable($phpc)) { + echo " 005-SessionsWeb AOT: skip (phpc not executable)\n"; + + return null; + } + + $sessionDir = sys_get_temp_dir().'/phpc_bench_sessionsweb_'.uniqid('', true); + if (!@mkdir($sessionDir, 0700, true) && !is_dir($sessionDir)) { + echo " 005-SessionsWeb AOT: skip (session temp dir)\n"; + + return null; + } + + $aotEnv = $benchEnv; + $aotEnv['PHP_COMPILER_SESSION_DIR'] = $sessionDir; + + $compileStart = microtime(true); + $build = runProcessCapturing( + [$phpc, 'build', '--project', $project], + $aotEnv, + $repoRoot + ); + $compileTime = microtime(true) - $compileStart; + + if (0 !== $build['exit']) { + $stderr = $build['stderr']; + if (\PHPCompiler\Cli\PhpcBuild::isUserClassAotBlocked($stderr)) { + echo ' 005-SessionsWeb AOT: skip (link blocked: '.trim(substr($stderr, 0, 120))."…)\n"; + } else { + echo " 005-SessionsWeb AOT: skip (phpc build --project exit {$build['exit']})\n"; + } + sessionsWebBenchCleanup($sessionDir); + + return null; + } + + if (!is_executable($binary)) { + echo " 005-SessionsWeb AOT: skip (binary missing after link)\n"; + sessionsWebBenchCleanup($sessionDir); + + return null; + } + + if (!sessionsWebAotFlashProbe($repoRoot, $binary, $aotEnv)) { + echo " 005-SessionsWeb AOT: skip (two-request flash probe failed)\n"; + sessionsWebBenchCleanup($sessionDir); + + return null; + } + + $iterations = 10; + $compiledStart = microtime(true); + for ($i = 0; $i < $iterations; ++$i) { + sessionsWebAotFlashProbe($repoRoot, $binary, $aotEnv); + } + $compiledTime = (microtime(true) - $compiledStart) / $iterations; + + sessionsWebBenchCleanup($sessionDir); + + return ['compile' => $compileTime, 'compiled' => $compiledTime]; +} + +/** + * @param array $baseEnv + */ +function sessionsWebAotFlashProbe(string $repoRoot, string $binary, array $baseEnv): bool +{ + $jar = new \PHPCompiler\CgiCookieJar(); + $empty = runProcessCapturing( + [$binary], + array_merge($baseEnv, \PHPCompiler\SessionsWebCgiEnv::getEmpty()), + $repoRoot + ); + if (0 !== $empty['exit']) { + return false; + } + $jar->absorbFromCgiOutput($empty['stdout']); + if (!$jar->hasCookie('PHPSESSID')) { + return false; + } + + $cookie = $jar->httpCookieHeader(); + $post = runProcessCapturing( + [$binary], + array_merge($baseEnv, \PHPCompiler\SessionsWebCgiEnv::postFlash('Saved'), ['HTTP_COOKIE' => $cookie]), + $repoRoot + ); + if (0 !== $post['exit']) { + return false; + } + + $flash = runProcessCapturing( + [$binary], + array_merge($baseEnv, \PHPCompiler\SessionsWebCgiEnv::getEmpty(), ['HTTP_COOKIE' => $jar->httpCookieHeader()]), + $repoRoot + ); + if (0 !== $flash['exit']) { + return false; + } + + return str_contains($flash['stdout'], 'Flash: Saved'); +} + +function sessionsWebBenchCleanup(string $sessionDir): void +{ + if (!is_dir($sessionDir)) { + return; + } + foreach (glob($sessionDir.'/sess_*') ?: [] as $file) { + @unlink($file); + } + @rmdir($sessionDir); +} + /** * @param list $argv * @param array $env @@ -543,7 +682,13 @@ function benchmarkExample(string $example, array $phpCmd, array $benchEnv, strin $compileTime = null; $compiledTime = null; - if ($llvmReady && !empty($profile['project_aot'])) { + if ($llvmReady && !empty($profile['sessions_web_project_aot'])) { + $sessionsAot = tryBenchmarkSessionsWebProjectAot($repoRoot, $benchEnv); + if (null !== $sessionsAot) { + $compileTime = $sessionsAot['compile']; + $compiledTime = $sessionsAot['compiled']; + } + } elseif ($llvmReady && !empty($profile['project_aot'])) { $projectAot = tryBenchmarkMiniWebAppProjectAot($repoRoot, $benchEnv, $profile); if (null !== $projectAot) { $compileTime = $projectAot['compile']; diff --git a/test/unit/RebuildExamples005RowTest.php b/test/unit/RebuildExamples005RowTest.php index 936db4f34..d08ad4003 100644 --- a/test/unit/RebuildExamples005RowTest.php +++ b/test/unit/RebuildExamples005RowTest.php @@ -29,5 +29,8 @@ public function testRebuildExamples005RowCheckerScriptDocumentsPolicy(): void $this->assertStringContainsString('005-SessionsWeb', $script); $this->assertStringContainsString('benchmark table start', $script); $this->assertStringContainsString('#1891', $script); + $this->assertStringContainsString('BENCH_SESSIONSWEB_AOT', $script); + $this->assertStringContainsString('sessions_web_aot_execute_probe', $script); + $this->assertStringContainsString('#1973', $script); } } diff --git a/test/unit/RebuildExamplesTest.php b/test/unit/RebuildExamplesTest.php index 10e41316e..0e85a5fa2 100644 --- a/test/unit/RebuildExamplesTest.php +++ b/test/unit/RebuildExamplesTest.php @@ -44,8 +44,11 @@ public function testRebuildExamplesDocumentsSessionsWebBenchmarkGate(): void $this->assertStringContainsString('BENCH_SESSIONSWEB', $script); $this->assertStringContainsString('SESSIONSWEB_LINT_GATE', $script); $this->assertStringContainsString('examples/005-SessionsWeb', $script); - $this->assertStringContainsString("'skip_aot' => true", $script); + $this->assertStringContainsString("'sessions_web_project_aot' => true", $script); + $this->assertStringContainsString('tryBenchmarkSessionsWebProjectAot', $script); + $this->assertStringContainsString('BENCH_SESSIONSWEB_AOT', $script); $this->assertStringContainsString('#1891', $script); + $this->assertStringContainsString('#1973', $script); $readme = file_get_contents(dirname(__DIR__, 2).'/examples/README.md'); $this->assertNotFalse($readme);