From c6994b501a68887e812b0d0b5f483bea9692c986 Mon Sep 17 00:00:00 2001 From: boriken72 Date: Tue, 23 Jun 2026 08:59:01 -0400 Subject: [PATCH 1/3] perf: cache app-shell template instead of re-reading index.html per request The `/`, `/index.html`, and `/session/` routes are the hottest navigations. Each request re-read the ~190 KB static/index.html from disk and re-applied two process-constant token substitutions (__WEBUI_VERSION__, __MAX_UPLOAD_BYTES__), then ran inject_extension_tags() (which itself reads the extension state + manifest from disk). WEBUI_VERSION and MAX_UPLOAD_BYTES are fixed for the process lifetime, so this caches the partially rendered template in an in-process cache keyed by (size, mtime_ns) -- mirroring the existing _STATIC_CACHE pattern in this file -- so a redeploy is still picked up without a restart. The per-session CSRF token and runtime extension tags are still applied per request, so output is byte-identical to before. Verified: byte-identical to the old inline render, CSRF still varies per request, cache invalidates on mtime change, ~5x faster shell render in a local microbenchmark. Adds tests/test_index_shell_template_cache.py. --- CHANGELOG.md | 4 ++ api/routes.py | 55 ++++++++++++--- tests/test_index_shell_template_cache.py | 87 ++++++++++++++++++++++++ 3 files changed, 138 insertions(+), 8 deletions(-) create mode 100644 tests/test_index_shell_template_cache.py diff --git a/CHANGELOG.md b/CHANGELOG.md index e74a3b4e..cb08b524 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ ## [Unreleased] +### Changed + +- **The app shell (`/`, `/index.html`, `/session/`) is now served from an in-process template cache instead of re-reading and re-rendering `static/index.html` (~190 KB) on every navigation.** These are the hottest routes; each request previously re-read the file from disk and re-applied two process-constant token substitutions (`__WEBUI_VERSION__`, `__MAX_UPLOAD_BYTES__`). The partially rendered template is now cached and invalidated on `(size, mtime_ns)` change — mirroring the existing static-asset cache — so a redeploy is still picked up without a restart. The per-session CSRF token and runtime extension tags are still applied per request, so output is byte-identical to before. Measured ~5× faster shell render in a local microbenchmark and removes per-request extension-manifest disk reads. + ## [v0.51.602] — 2026-06-23 — Release VI (sidebar fetches only the active source bucket) ### Changed diff --git a/api/routes.py b/api/routes.py index 019fbf1c..31c6ac34 100644 --- a/api/routes.py +++ b/api/routes.py @@ -8849,6 +8849,48 @@ def _save_saved_prompts(prompts: list) -> None: p.write_text(json.dumps(prompts, ensure_ascii=False, indent=2), encoding="utf-8") +# In-process cache for the app-shell template. The `/`, `/index.html`, and +# `/session/` routes are the hottest navigations and each re-read the +# ~190 KB static/index.html from disk and re-ran the two process-constant +# substitutions (__WEBUI_VERSION__, __MAX_UPLOAD_BYTES__) on every request. +# Those values are fixed for the process lifetime, so we cache the partially +# rendered template here, keyed by (size, nanosecond mtime) exactly like +# _STATIC_CACHE so a redeploy is picked up without a restart. The two values +# that genuinely vary per request — the per-session CSRF token and the runtime +# extension tags (inject_extension_tags) — are still applied on each request +# against the cached base, so caching changes no observable output. +_INDEX_SHELL_CACHE: dict = {} +_INDEX_SHELL_CACHE_LOCK = threading.Lock() + + +def _render_index_shell_base() -> str: + """Return static/index.html with the process-constant tokens substituted. + + Cached and invalidated on (size, mtime_ns) change. The CSRF token and + extension-tag injection are intentionally NOT applied here — they vary per + request and are applied by the caller against this base string. + """ + from api.updates import WEBUI_VERSION + + st = _INDEX_HTML_PATH.stat() + sig = (st.st_size, st.st_mtime_ns) + with _INDEX_SHELL_CACHE_LOCK: + cached = _INDEX_SHELL_CACHE.get("base") + if cached and cached[0] == sig: + return cached[1] + from urllib.parse import quote + + version_token = quote(WEBUI_VERSION, safe="") + base = ( + _INDEX_HTML_PATH.read_text(encoding="utf-8") + .replace("__WEBUI_VERSION__", version_token) + .replace("__MAX_UPLOAD_BYTES__", str(MAX_UPLOAD_BYTES)) + ) + with _INDEX_SHELL_CACHE_LOCK: + _INDEX_SHELL_CACHE["base"] = (sig, base) + return base + + def handle_get(handler, parsed) -> bool: """Handle all GET routes. Returns True if handled, False for 404.""" @@ -8870,9 +8912,6 @@ def handle_get(handler, parsed) -> bool: if parsed.path in ("/", "/index.html") or parsed.path.startswith("/session/"): try: - from urllib.parse import quote - from api.updates import WEBUI_VERSION - version_token = quote(WEBUI_VERSION, safe="") from api.extensions import inject_extension_tags csrf_token = "" @@ -8886,11 +8925,11 @@ def handle_get(handler, parsed) -> bool: except Exception: csrf_token = "" - html = ( - _INDEX_HTML_PATH.read_text(encoding="utf-8") - .replace("__WEBUI_VERSION__", version_token) - .replace("__MAX_UPLOAD_BYTES__", str(MAX_UPLOAD_BYTES)) - .replace("__CSRF_TOKEN_JSON__", json.dumps(csrf_token)) + # The disk read + process-constant token substitutions are cached; + # only the per-session CSRF token and per-request extension tags are + # applied here (see _render_index_shell_base). + html = _render_index_shell_base().replace( + "__CSRF_TOKEN_JSON__", json.dumps(csrf_token) ) return t( handler, diff --git a/tests/test_index_shell_template_cache.py b/tests/test_index_shell_template_cache.py new file mode 100644 index 00000000..de279a55 --- /dev/null +++ b/tests/test_index_shell_template_cache.py @@ -0,0 +1,87 @@ +"""Regression tests for the app-shell template cache (_render_index_shell_base). + +The `/`, `/index.html`, and `/session/` routes are the hottest navigations. +Each previously re-read the ~190 KB static/index.html from disk and re-ran the +two process-constant token substitutions (__WEBUI_VERSION__, +__MAX_UPLOAD_BYTES__) on every request. _render_index_shell_base() caches the +partially rendered template, keyed by (size, mtime_ns) like _STATIC_CACHE, while +the per-session CSRF token and per-request extension tags stay per-request. + +These tests guard the load-bearing invariant: caching must change no observable +output, only avoid redundant work. +""" +from __future__ import annotations + +import json +import os +import time +from urllib.parse import quote + +import api.routes as routes +from api.updates import WEBUI_VERSION + + +def _old_inline_render(csrf_token: str) -> str: + """Reproduce the pre-cache inline render exactly, for equivalence checks.""" + return ( + routes._INDEX_HTML_PATH.read_text(encoding="utf-8") + .replace("__WEBUI_VERSION__", quote(WEBUI_VERSION, safe="")) + .replace("__MAX_UPLOAD_BYTES__", str(routes.MAX_UPLOAD_BYTES)) + .replace("__CSRF_TOKEN_JSON__", json.dumps(csrf_token)) + ) + + +def test_base_substitutes_process_constants_but_not_csrf(): + base = routes._render_index_shell_base() + assert "__WEBUI_VERSION__" not in base + assert "__MAX_UPLOAD_BYTES__" not in base + # CSRF must remain a placeholder — it varies per request and is applied by + # the caller, not baked into the shared cache. + assert "__CSRF_TOKEN_JSON__" in base + + +def test_cached_render_is_byte_identical_to_old_inline(): + csrf = "test-csrf-abc123" + new = routes._render_index_shell_base().replace( + "__CSRF_TOKEN_JSON__", json.dumps(csrf) + ) + assert new == _old_inline_render(csrf) + + +def test_csrf_token_varies_per_request(): + a = routes._render_index_shell_base().replace( + "__CSRF_TOKEN_JSON__", json.dumps("AAA") + ) + b = routes._render_index_shell_base().replace( + "__CSRF_TOKEN_JSON__", json.dumps("BBB") + ) + assert json.dumps("AAA") in a + assert json.dumps("BBB") in b + assert a != b + + +def test_second_call_returns_cached_object(): + # Warm the cache, then assert the identical object comes back (no re-read). + first = routes._render_index_shell_base() + second = routes._render_index_shell_base() + assert first is second + + +def test_cache_invalidates_on_mtime_change(tmp_path, monkeypatch): + # Point the module at a temp copy so we can mutate its mtime safely. + src = routes._INDEX_HTML_PATH.read_text(encoding="utf-8") + fake = tmp_path / "index.html" + fake.write_text(src, encoding="utf-8") + monkeypatch.setattr(routes, "_INDEX_HTML_PATH", fake) + # Reset the shared cache so this test is order-independent. + monkeypatch.setattr(routes, "_INDEX_SHELL_CACHE", {}) + + routes._render_index_shell_base() + sig_before = routes._INDEX_SHELL_CACHE["base"][0] + + future = time.time() + 5 + os.utime(fake, (future, future)) + routes._render_index_shell_base() + sig_after = routes._INDEX_SHELL_CACHE["base"][0] + + assert sig_after != sig_before From 9cc3f30d82eee1aee2565f2c42ceb34c56d8f57b Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Tue, 23 Jun 2026 13:27:40 +0000 Subject: [PATCH 2/3] test(#4774): point asset-version-encoding test at relocated shell-render helper #4774 moved the version-token substitution from the index route handler into the cached _render_index_shell_base() helper. quote(WEBUI_VERSION, safe='') encoding is intact (verified), just relocated, so the source-marker test scanning the route block no longer found it. Updated to check the helper. Maintainer test-fix on boriken72's PR. --- tests/test_pwa_manifest_sw.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/tests/test_pwa_manifest_sw.py b/tests/test_pwa_manifest_sw.py index 0e10001f..380e110f 100644 --- a/tests/test_pwa_manifest_sw.py +++ b/tests/test_pwa_manifest_sw.py @@ -344,13 +344,24 @@ class TestIndexHtmlIntegration: def test_index_route_url_encodes_asset_version(self): src = ROUTES.read_text(encoding="utf-8") - idx = src.find('parsed.path in ("/", "/index.html")') - if idx == -1: - idx = src.find('parsed.path.startswith("/session/")') - assert idx != -1, "routes.py must handle /, /index.html, and /session/" - block = src[idx:idx + 800] + # #4774 moved the app-shell render (incl. the version-token substitution) + # out of the route handler and into the cached `_render_index_shell_base()` + # helper. The security property — the cache-busting version token is + # URL-encoded before it's injected into script src / SW registration — must + # still hold, so assert it in whichever location renders the shell. Check the + # shell-render helper first (its current home), then fall back to the route + # handler block for older layouts. + helper_idx = src.find("def _render_index_shell_base") + if helper_idx != -1: + block = src[helper_idx:helper_idx + 1200] + else: + idx = src.find('parsed.path in ("/", "/index.html")') + if idx == -1: + idx = src.find('parsed.path.startswith("/session/")') + assert idx != -1, "routes.py must handle /, /index.html, and /session/" + block = src[idx:idx + 800] assert "quote(WEBUI_VERSION, safe=\"\")" in block, ( - "index route must URL-encode the cache-busting version token before " + "the app-shell render must URL-encode the cache-busting version token before " "injecting it into script src attributes and service worker registration" ) From f3a509e2e9048125e7a5ae463f99e192ce9ed966 Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Tue, 23 Jun 2026 13:39:07 +0000 Subject: [PATCH 3/3] =?UTF-8?q?Release=20v0.51.603=20=E2=80=94=20Release?= =?UTF-8?q?=20VJ=20(cache=20app-shell=20template,=20#4774)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb08b524..47b5a2af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,11 @@ ## [Unreleased] +## [v0.51.603] — 2026-06-23 — Release VJ (cache the app-shell template) + ### Changed -- **The app shell (`/`, `/index.html`, `/session/`) is now served from an in-process template cache instead of re-reading and re-rendering `static/index.html` (~190 KB) on every navigation.** These are the hottest routes; each request previously re-read the file from disk and re-applied two process-constant token substitutions (`__WEBUI_VERSION__`, `__MAX_UPLOAD_BYTES__`). The partially rendered template is now cached and invalidated on `(size, mtime_ns)` change — mirroring the existing static-asset cache — so a redeploy is still picked up without a restart. The per-session CSRF token and runtime extension tags are still applied per request, so output is byte-identical to before. Measured ~5× faster shell render in a local microbenchmark and removes per-request extension-manifest disk reads. +- **The app shell (`/`, `/index.html`, `/session/`) is now served from an in-process template cache instead of re-reading and re-rendering `static/index.html` (~190 KB) on every navigation.** These are the hottest routes; each request previously re-read the file from disk and re-applied two process-constant token substitutions (`__WEBUI_VERSION__`, `__MAX_UPLOAD_BYTES__`). The partially rendered template is now cached and invalidated on `(size, mtime_ns)` change — mirroring the existing static-asset cache — so a redeploy is still picked up without a restart. The per-session CSRF token and runtime extension tags are still applied per request, so output is byte-identical to before. Measured ~5× faster shell render in a local microbenchmark and removes per-request extension-manifest disk reads. Thanks @boriken72. (#4774) ## [v0.51.602] — 2026-06-23 — Release VI (sidebar fetches only the active source bucket)