diff --git a/CHANGELOG.md b/CHANGELOG.md index d653acfd..784601ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Service-health pings are now cached across requests.** `HealthService::statusFor()`'s 10 s memo lived in a per-object array, which the classic (non-worker) FrankenPHP throws away with each request — so every topbar/dashboard health poll, from every open tab, re-pinged all configured services with sequential blocking calls. The verdicts are now shared through `cache.app` (same 10 s TTL): the whole install performs at most one probe sweep per window, and `invalidate()` (admin "Test connection" / settings save) rotates a generation token so stale verdicts can't outlive a reconfiguration. - **Static assets get real browser-cache headers.** Caddy now serves AssetMapper-compiled `/assets/*` (content-hashed filenames) with `Cache-Control: public, max-age=31536000, immutable`, and the unfingerprinted `/static/*` vendor bundles (Tabler, Chart.js) plus `/img/*` with a one-day TTL — repeat page loads stop re-negotiating ~600 KB of CSS/JS. - **Prod cache pre-warmed at image build.** The Dockerfile runs `cache:warmup` after `asset-map:compile`, so the first request after a container (re)start no longer pays the 1–3 s container/route/Twig compile. Env vars stay runtime-resolved placeholders, so the boot-generated `APP_SECRET` doesn't invalidate the baked cache. +- **OPcache preloading of the framework + app class graph.** `php.ini` now sets `opcache.preload` to Symfony's `config/preload.php` (backed by the compiled `var/cache/prod` preload file that the image's build-time `cache:warmup` already generates), so ~1,300 framework/app classes are linked into shared memory once at boot instead of on first use. FrankenPHP reports `PHP_SAPI` as `frankenphp` (not `embed`), so Symfony's preload file runs rather than short-circuiting — verified in a fresh container (1336 scripts preloaded, clean boot, health endpoint OK). Since the code is baked into the image and `opcache.validate_timestamps=0`, the preloaded graph stays valid for the container's lifetime. - **Faster Radarr / Sonarr library pages.** The heavy `getMovies()` / `getSeries()` payload is now cached per instance for 45 s (`MediaLibraryCache`) instead of being re-fetched and re-normalised on every visit, and the per-page status / queue / indexers / health / calendar calls run in a single `curl_multi` batch (`multiGet()`) rather than sequentially. Cold loads are unchanged, but revisits within the window are roughly 3× faster, and a slow or unreachable instance now costs one timeout window for the whole page instead of stacking one timeout per call. Empty results are not cached and library mutations invalidate the entry, so user changes still show immediately. Same per-handle semantics as the existing `get()` (SSRF protocol guard, connect/total timeouts, per-instance circuit breaker). ### Internal diff --git a/docker/frankenphp/php.ini b/docker/frankenphp/php.ini index cd2f5d24..f54fbb31 100644 --- a/docker/frankenphp/php.ini +++ b/docker/frankenphp/php.ini @@ -35,6 +35,16 @@ opcache.max_accelerated_files = 10000 opcache.revalidate_freq = 0 opcache.validate_timestamps = 0 +; Preload the framework + app class graph into shared memory at boot, so no +; request pays to link them. Symfony emits config/preload.php (which requires +; the compiled var/cache/prod/*.preload.php) during the cache:warmup already +; run at image build, so the referenced files exist in the image. Since the +; code is baked and validate_timestamps=0, the preloaded graph is stable for +; the container's lifetime. preload_user is required when PHP starts as root +; (s6 runs the init as root, then drops to www-data for the server). +opcache.preload = /var/www/html/config/preload.php +opcache.preload_user = www-data + ; Realpath cache (Symfony perf) realpath_cache_size = 4096K realpath_cache_ttl = 600