From 38b08dc8e411b8e2078e753833cc4ccc84195abc Mon Sep 17 00:00:00 2001 From: Sebastian Mendel Date: Thu, 22 Jan 2026 02:05:06 +0100 Subject: [PATCH] perf: add DI container caching for faster CLI startup Cache the compiled Symfony DI container to /tmp/guides-container-cache/. On subsequent runs, loads pre-compiled PHP class instead of rebuilding the entire container (config parsing, service registration, compilation). Cache key includes vendor dir, working dir, extensions, and configs to ensure cache invalidation when configuration changes. See https://cybottm.github.io/render-guides/ for benchmark data. --- .../DependencyInjection/ContainerFactory.php | 75 ++++++++++++++++++- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/packages/guides-cli/src/DependencyInjection/ContainerFactory.php b/packages/guides-cli/src/DependencyInjection/ContainerFactory.php index 866639dd1..630d20851 100644 --- a/packages/guides-cli/src/DependencyInjection/ContainerFactory.php +++ b/packages/guides-cli/src/DependencyInjection/ContainerFactory.php @@ -22,20 +22,35 @@ use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Dumper\PhpDumper; +use Symfony\Component\DependencyInjection\Exception\RuntimeException as DIRuntimeException; use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; +use function array_keys; use function array_merge; +use function assert; use function class_exists; +use function file_exists; +use function file_put_contents; +use function function_exists; use function getcwd; use function implode; use function is_a; +use function is_dir; +use function md5; +use function mkdir; +use function opcache_invalidate; use function rtrim; +use function serialize; use function sprintf; use function strrchr; use function substr; final class ContainerFactory { + private const CACHE_DIR = '/tmp/guides-container-cache'; + private const CACHE_CLASS = 'CachedGuidesContainer'; + private readonly ContainerBuilder $container; private readonly XmlFileLoader $configLoader; @@ -78,16 +93,72 @@ public function addConfigFile(string $filePath): void public function create(string $vendorDir): Container { - $this->processConfig(); + $cacheKey = $this->generateCacheKey($vendorDir); + $cacheFile = self::CACHE_DIR . '/' . self::CACHE_CLASS . '_' . $cacheKey . '.php'; + $cacheClass = self::CACHE_CLASS . '_' . $cacheKey; + + // Try to load cached container + if (file_exists($cacheFile)) { + require_once $cacheFile; + if (class_exists($cacheClass, false)) { + $container = new $cacheClass(); + assert($container instanceof Container); + + return $container; + } + } + // Build container + $this->processConfig(); $this->container->setParameter('vendor_dir', $vendorDir); $this->container->setParameter('working_directory', rtrim(getcwd(), '/')); - $this->container->compile(true); + // Try to cache the compiled container (may fail if container has object parameters) + try { + $this->cacheContainer($cacheFile, $cacheClass); + } catch (DIRuntimeException) { + // Container cannot be cached (has object/resource parameters), continue without caching + } + return $this->container; } + private function generateCacheKey(string $vendorDir): string + { + $workingDir = getcwd(); + $configData = [ + 'vendor_dir' => $vendorDir, + 'working_dir' => $workingDir !== false ? rtrim($workingDir, '/') : '', + 'extensions' => array_keys($this->registeredExtensions), + 'configs' => serialize($this->configs), + ]; + + return substr(md5(serialize($configData)), 0, 12); + } + + private function cacheContainer(string $cacheFile, string $cacheClass): void + { + if (!is_dir(self::CACHE_DIR)) { + @mkdir(self::CACHE_DIR, 0755, true); + } + + $dumper = new PhpDumper($this->container); + $code = $dumper->dump([ + 'class' => $cacheClass, + 'base_class' => Container::class, + ]); + + file_put_contents($cacheFile, $code); + + // Invalidate opcache for the new file + if (!function_exists('opcache_invalidate')) { + return; + } + + opcache_invalidate($cacheFile, true); + } + /** @param array $config */ private function registerExtension(ExtensionInterface $extension, array $config): void {