From a21073aefeec27540832a3255c7e9721f4c68e8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brian=20Sch=C3=A4ffner?= Date: Fri, 23 Oct 2026 23:43:51 +0200 Subject: [PATCH] perf(cache): read the immutable cache policy once per fingerprinter immutable() re-read the environment and the build ID for every bundle in fingerprint() and for every freshness check, and the discovery manifest created a new fingerprinter per call. Memoize the policy per instance and document that immutable release snapshots should enable it to skip the per-request config and discovery scans. --- README.md | 6 +++++- docs/dependency-injection.md | 5 +++++ src/Discovery/KernelPackageManifestCache.php | 5 ++++- src/Kernel/ContainerResourceFingerprinter.php | 10 ++++++++++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 978e782..bd958eb 100644 --- a/README.md +++ b/README.md @@ -201,7 +201,11 @@ hit. Imported configuration resources are still validated so service wiring changes invalidate the runtime container. Deployment rollovers can use `SYMPRESS_KERNEL_BUILD_ID` to force a new cache identity, and `SYMPRESS_KERNEL_VALIDATE_SOURCE_RESOURCES=1` opts back into source-level -freshness checks when the request-time filesystem cost is acceptable. +freshness checks when the request-time filesystem cost is acceptable. Immutable +release snapshots should set `SYMPRESS_KERNEL_IMMUTABLE_CACHE=1` together with a +build ID: that skips the per-request config and discovery directory scans (about +six to eight filesystem calls per bundle) and makes the build ID the only cache +boundary. Bundle discovery also stores the matching Composer package manifest under the kernel cache directory. The manifest is invalidated when Composer metadata such diff --git a/docs/dependency-injection.md b/docs/dependency-injection.md index c765b5b..107cf93 100644 --- a/docs/dependency-injection.md +++ b/docs/dependency-injection.md @@ -1041,6 +1041,11 @@ config, and discovery freshness scans and requires a nonempty immutable and **every** code/config/Composer change changes that identity. A build ID alone does not disable normal config validation. Missing identity is an error; this optimization must never silently hide edits in a mutable installation. +Mutable warm hits otherwise cost one `stat()` per bundle `composer.json` and per +consumed config file plus one directory listing per existing config directory +(about six to eight filesystem calls per bundle), so immutable release +deployments should enable the policy. Runtime's `kernel-cache --generate-build-id` +produces a matching identity. Bundle extensions are pristine prototypes cloned for each builder. Runtime compilation copies the mutable definition/argument graph so console lint/debug diff --git a/src/Discovery/KernelPackageManifestCache.php b/src/Discovery/KernelPackageManifestCache.php index cb089d6..6a36acc 100644 --- a/src/Discovery/KernelPackageManifestCache.php +++ b/src/Discovery/KernelPackageManifestCache.php @@ -13,6 +13,7 @@ final class KernelPackageManifestCache { /** @var array> */ private array $metadata = []; + private ?ContainerResourceFingerprinter $fingerprints = null; /** @return array> */ public function metadata(): array @@ -187,7 +188,9 @@ private function buildIdentity(): string private function immutable(): bool { - return (new ContainerResourceFingerprinter((string) $this->projectDir, (string) $this->environment, false))->immutable(); + $this->fingerprints ??= new ContainerResourceFingerprinter((string) $this->projectDir, (string) $this->environment, false); + + return $this->fingerprints->immutable(); } private function rootComposerFile(): string diff --git a/src/Kernel/ContainerResourceFingerprinter.php b/src/Kernel/ContainerResourceFingerprinter.php index dfca0b5..cba4430 100644 --- a/src/Kernel/ContainerResourceFingerprinter.php +++ b/src/Kernel/ContainerResourceFingerprinter.php @@ -12,6 +12,8 @@ final class ContainerResourceFingerprinter { + private ?bool $immutable = null; + public function __construct( private readonly string $projectDir, private readonly string $environment, @@ -233,14 +235,22 @@ public function shouldValidateCachedSourceResources(): bool public function immutable(): bool { + // The policy is read once per fingerprinter; fingerprint() asks for it per bundle. + if ($this->immutable !== null) { + return $this->immutable; + } $enabled = $_SERVER['SYMPRESS_KERNEL_IMMUTABLE_CACHE'] ?? $_ENV['SYMPRESS_KERNEL_IMMUTABLE_CACHE'] ?? getenv('SYMPRESS_KERNEL_IMMUTABLE_CACHE'); if (filter_var($enabled, FILTER_VALIDATE_BOOL) !== true) { + $this->immutable = false; + return false; } $build = defined('SYMPRESS_KERNEL_BUILD_ID') ? constant('SYMPRESS_KERNEL_BUILD_ID') : ($_SERVER['SYMPRESS_KERNEL_BUILD_ID'] ?? $_ENV['SYMPRESS_KERNEL_BUILD_ID'] ?? getenv('SYMPRESS_KERNEL_BUILD_ID')); if (!is_string($build) || $build === '') { throw new \RuntimeException('Immutable kernel cache requires an explicit deployment build ID.'); } + $this->immutable = true; + return true; }