Speed up loading compiled templates that OPcache already holds

This commit is contained in:
Nicolas Grekas
2026-09-25 16:54:20 +02:00
committed by Fabien Potencier
parent c471d2ad58
commit ef3780c2e0
3 changed files with 64 additions and 1 deletions
+1
View File
@@ -10,6 +10,7 @@
* Fix the `deprecated` tag generating invalid PHP for an integer message
* Add a third argument to `FilesystemLoader::addPath()` and `FilesystemLoader::prependPath()` to skip checking that the directory exists
* Speed up traversing nodes with node visitors
* Speed up loading compiled templates that OPcache already holds
# 3.30.0 (2026-09-25)
+8 -1
View File
@@ -22,11 +22,18 @@ class FilesystemCache implements CacheInterface, RemovableCacheInterface
private $directory;
private $options;
private bool $checkOpcache;
public function __construct(string $directory, int $options = 0)
{
$this->directory = rtrim($directory, '\/').'/';
$this->options = $options;
// OPcache can still hold a compiled file removed from disk, which FORCE_BYTECODE_INVALIDATION wants compiled again.
// Its API also warns on each call when restricted, and it checks the file anyway when it validates timestamps on each call.
$this->checkOpcache = !($options & self::FORCE_BYTECODE_INVALIDATION)
&& \function_exists('opcache_is_script_cached')
&& !\ini_get('opcache.restrict_api')
&& (!filter_var(\ini_get('opcache.validate_timestamps'), \FILTER_VALIDATE_BOOLEAN) || \ini_get('opcache.revalidate_freq'));
}
public function generateKey(string $name, string $className): string
@@ -38,7 +45,7 @@ class FilesystemCache implements CacheInterface, RemovableCacheInterface
public function load(string $key): void
{
if (is_file($key)) {
if (($this->checkOpcache && opcache_is_script_cached($key)) || is_file($key)) {
@include_once $key;
}
}
+55
View File
@@ -74,6 +74,21 @@ class FilesystemTest extends TestCase
$this->assertFalse(class_exists($this->className, false));
}
public function testLoadSkipsTheFileCheckWhenOpcacheHasTheFile(): void
{
$this->assertSame('loaded', $this->loadInOpcacheProcess(0, true));
}
public function testLoadChecksTheFileWhenBytecodeInvalidationIsForced(): void
{
$this->assertSame('', $this->loadInOpcacheProcess(FilesystemCache::FORCE_BYTECODE_INVALIDATION, true));
}
public function testLoadDoesNotWarnWhenTheOpcacheApiIsRestricted(): void
{
$this->assertSame('loaded', $this->loadInOpcacheProcess(0, false, '/nonexistent'));
}
public function testWrite(): void
{
$key = $this->directory.'/cache/cachefile.php';
@@ -202,4 +217,44 @@ class FilesystemTest extends TestCase
'{{class_name}}' => $this->className,
]);
}
private function loadInOpcacheProcess(int $options, bool $removeCachedFile, string $restrictApi = ''): string
{
if (!\function_exists('opcache_compile_file')) {
$this->markTestSkipped('OPcache is not loaded.');
}
$code = <<<'PHP'
require $argv[1];
$cache = new Twig\Cache\FilesystemCache($argv[2], (int) $argv[3]);
$key = $cache->generateKey('index', 'Foo');
$cache->write($key, '<?php echo "loaded";');
if ($argv[4]) {
opcache_compile_file($key);
unlink($key);
}
$cache->load($key);
PHP;
$process = proc_open([
\PHP_BINARY,
'-d', 'opcache.enable=1',
'-d', 'opcache.enable_cli=1',
'-d', 'opcache.file_update_protection=0',
'-d', 'opcache.validate_timestamps=0',
'-d', 'opcache.restrict_api='.$restrictApi,
'-d', 'display_errors=1',
'-d', 'error_reporting=-1',
'-r', $code,
\dirname(__DIR__, 2).'/vendor/autoload.php',
$this->directory,
(string) $options,
$removeCachedFile ? '1' : '0',
], [1 => ['pipe', 'w'], 2 => ['redirect', 1]], $pipes);
$output = stream_get_contents($pipes[1]);
proc_close($process);
return $output;
}
}